· 2 years ago · Feb 01, 2023, 01:50 PM
1/**
2 * Create the XenForo namespace
3 * @package XenForo
4 */
5var XenForo = {};
6
7// _XF_JS_UNCOMPRESSED_TEST_ - do not edit/remove
8
9if (jQuery === undefined) jQuery = $ = {};
10if ($.tools === undefined) console.error('jQuery Tools is not loaded.');
11
12
13
14function 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
42var 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
52var supports_html5_storage = function () {
53 try {
54 return 'localStorage' in window && window['localStorage'] !== null;
55 } catch (e) {
56 return false;
57 }
58};
59
60function 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
70function 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>></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, '&')
2665 .replace(/"/g, '"')
2666 .replace(/</g, '<')
2667 .replace(/>/g, '>');
2668 },
2669
2670 htmlEntityDecode: function (string) {
2671 return (String(string) || '')
2672 .replace(/"/g, '"')
2673 .replace(/</g, '<')
2674 .replace(/>/g, '>')
2675 .replace(/&/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 ? document.body.children : document).on(
4861 'click',
4862 function (clickEvent) {
4863 if (!$(clickEvent.target).closest('.QuickSearch').length) {
4864 console.log('Hide quick search menu');
4865
4866 $('#QuickSearch').removeClass('show');
4867 $('#QuickSearchPlaceholder').removeClass('hide');
4868
4869 $target.find('.secondaryControls').slideUp(XenForo.speed.xfast, function () {
4870 $target.removeClass('active');
4871 if ($.browser.msie) {
4872 $('body').css('zoom', 1);
4873 setTimeout(function () {
4874 $('body').css('zoom', '');
4875 }, 100);
4876 }
4877 });
4878 }
4879 }
4880 );
4881 }
4882
4883 $target.addClass('active');
4884 $target.find('.secondaryControls').slideDown(0);
4885 },
4886
4887 input: function (e) {
4888 if ($(e.currentTarget).val()) {
4889 this.$target.addClass('hasInputValue');
4890 this.$clearButton.addClass('show')
4891 }
4892 else {
4893 this.$target.removeClass('hasInputValue');
4894 this.$clearButton.removeClass('show')
4895 }
4896
4897 if (this.updateTimer) {
4898 clearTimeout(this.updateTimer);
4899 }
4900
4901 const self = this;
4902 this.updateTimer = setTimeout(function () {
4903 if (self.$queryInput.val().length < 3) {
4904 self.$searchResults.html('');
4905
4906 return;
4907 }
4908
4909 this.xhr = XenForo.ajax(
4910 self.$target.data('member-search-url'),
4911 {
4912 username: self.$queryInput.val()
4913 },
4914 function (data, status) {
4915 if (XenForo.hasResponseError(data, status)) {
4916 return false;
4917 }
4918 else {
4919 self.$searchResults.html('');
4920 $(data.templateHtml).xfInsert('appendTo', self.$searchResults);
4921 }
4922 }
4923 );
4924 }, 500);
4925 },
4926
4927 clearQueryInput: function () {
4928 this.$queryInput.val('').trigger('input');
4929 this.$searchResults.html('');
4930
4931 if (this.xhr) {
4932 this.xhr.abort();
4933 this.xhr = null;
4934 }
4935 }
4936 };
4937
4938 // *********************************************************************
4939
4940 XenForo.configureTooltipRtl = function (config) {
4941 if (config.offset !== undefined) {
4942 config.offset = XenForo.switchOffsetRTL(config.offset);
4943 }
4944
4945 if (config.position !== undefined) {
4946 config.position = XenForo.switchStringRTL(config.position);
4947 }
4948
4949 return config;
4950 };
4951
4952 /**
4953 * Wrapper for jQuery Tools Tooltip
4954 *
4955 * @param $element
4956 */
4957 XenForo.Tooltip = function ($element) {
4958 if ($element.hasClass('PopupTooltip')) {
4959 var isFirstShown = true;
4960 var isElementLoaded = false;
4961 var next = $element.next($($element.data('content')))
4962 var $content = $('<div>').append(next.clone()).html();
4963 if (!next.hasClass('control')) {
4964 next.remove()
4965 }
4966
4967 const like_link = $element.data('likes-url');
4968
4969 if (like_link) {
4970 tippy($element.get(), {
4971 content: "",
4972 theme: 'popup',
4973 interactive: true,
4974 arrow: true,
4975 maxWidth: $element.data('width') || 250,
4976 animation: 'shift-toward',
4977 hideOnClick: XenForo.isTouchBrowser(),
4978 placement: $element.data('placement') || 'top',
4979 zIndex: $element.closest('.xenOverlay').length ? 11111 : 9000, // overlay fix
4980 trigger: XenForo.isTouchBrowser() ? 'click' : 'mouseenter focus', // Touch fix
4981 onTrigger: function () {
4982 if (!isElementLoaded || !$element.get(0)._tippy.props.content) {
4983 setTimeout(() => {
4984 $element.get(0)._tippy.hide();
4985 }, 0)
4986
4987 XenForo.ajax(like_link, {}, function (ajaxData) {
4988 if (ajaxData.templateHtml) {
4989 isElementLoaded = true;
4990 $element.get(0)._tippy.setContent(ajaxData.templateHtml);
4991 if ($element.is(':hover')) {
4992 $element.get(0)._tippy.show();
4993 }
4994 }
4995 }.bind(this));
4996 }
4997 },
4998 onShown: function () {
4999 if ($('body').hasClass('iOS')) {
5000 $element.click();
5001 }
5002 if (!isFirstShown) return;
5003 $('.' + $(this.content).attr('class')).xfActivate();
5004 isFirstShown = false;
5005 }
5006 });
5007 } else {
5008 tippy($element.get(), {
5009 content: $content,
5010 theme: 'popup',
5011 interactive: true,
5012 arrow: true,
5013 maxWidth: $element.data('width') || 250,
5014 animation: 'shift-toward',
5015 hideOnClick: XenForo.isTouchBrowser(),
5016 placement: $element.data('placement') || 'top',
5017 zIndex: $element.closest('.xenOverlay').length ? 11111 : 9000, // overlay fix
5018 trigger: XenForo.isTouchBrowser() ? 'click' : 'mouseenter focus', // Touch fix
5019
5020 onShown: function () {
5021 if ($('body').hasClass('iOS')) {
5022 $element.click();
5023 }
5024
5025 if (!isFirstShown) return;
5026 $('.' + $(this.content).attr('class')).xfActivate();
5027 isFirstShown = false;
5028 }
5029 });
5030 }
5031 } else {
5032 var title = $element.attr('title');
5033 let cachedtitle = $element.attr('data-cachedtitle')
5034 if (!title && cachedtitle) {
5035 title = cachedtitle // if restored from cache (title is empty)
5036 }
5037 $element.attr('data-cachedtitle', title)
5038 $element.attr('title', '');
5039 tippy($element.get(), {
5040 arrow: true,
5041 animation: 'shift-toward',
5042 distance: 5,
5043 maxWidth: 250,
5044 zIndex: 11111,
5045 placement: $element.data('placement') || 'top',
5046 content: XenForo.htmlspecialchars(title),
5047 trigger: XenForo.isTouchBrowser() ? 'click' : 'mouseenter focus', // Touch fix
5048
5049 onShow: function () {
5050 if ($('body').hasClass('iOS')) {
5051 $element.click();
5052 }
5053 },
5054 });
5055 }
5056 };
5057 // *********************************************************************
5058
5059 /*XenForo.StatusTooltip = function ($element) {
5060 if ($element.attr('title')) {
5061 var title = XenForo.htmlspecialchars($element.attr('title'));
5062
5063 $element.attr('title', title).tooltip(XenForo.configureTooltipRtl(
5064 {
5065 effect: 'slide',
5066 slideOffset: 30,
5067 position: 'bottom left',
5068 offset: [10, 1],
5069 tipClass: 'xenTooltip statusTip',
5070 layout: '<div><span class="arrow" /></div>'
5071 }));
5072 }
5073 };*/
5074
5075 // *********************************************************************
5076
5077 /*XenForo.NodeDescriptionTooltip = function ($title) {
5078 var description = $title.data('description');
5079
5080 if (description && $(description).length) {
5081 var $description = $(description)
5082 .addClass('xenTooltip nodeDescriptionTip')
5083 .appendTo('body')
5084 .append('<span class="arrow" />');
5085
5086 $title.tooltip(XenForo.configureTooltipRtl(
5087 {
5088 effect: 'slide',
5089 slideOffset: 30,
5090 offset: [30, 10],
5091 slideInSpeed: XenForo.speed.xfast,
5092 slideOutSpeed: 50 * XenForo._animationSpeedMultiplier,
5093
5094 /!*effect: 'fade',
5095 fadeInSpeed: XenForo.speed.xfast,
5096 fadeOutSpeed: XenForo.speed.xfast,*!/
5097
5098 predelay: 250,
5099 position: 'bottom right',
5100 tip: description,
5101
5102 onBeforeShow: function () {
5103 if (!$title.data('tooltip-shown')) {
5104 if ($(window).width() < 600) {
5105 var conf = $title.data('tooltip').getConf();
5106 conf.slideOffset = 0;
5107 conf.effect = 'toggle';
5108 conf.offset = [20, -$title.width()];
5109 conf.position = ['top', 'right'];
5110
5111 if (XenForo.isRTL()) {
5112 conf.offset[1] *= -1;
5113 conf.position[1] = 'left';
5114 }
5115
5116 $description.addClass('arrowBottom');
5117 }
5118
5119 $title.data('tooltip-shown', true);
5120 }
5121 }
5122 }));
5123 $title.click(function () {
5124 $(this).data('tooltip').hide();
5125 });
5126 }
5127 };*/
5128
5129 // *********************************************************************
5130
5131 XenForo.AccountMenu = function ($menu) {
5132 $menu.find('.submitUnit').hide();
5133
5134 $menu.find('.StatusEditor').focus(function (e) {
5135 if ($menu.is(':visible')) {
5136 $menu.find('.submitUnit').show();
5137 }
5138 });
5139 };
5140
5141 // *********************************************************************
5142
5143 XenForo.FollowLink = function ($link) {
5144 $link.click(function (e) {
5145 e.preventDefault();
5146
5147 $link.get(0).blur();
5148
5149 XenForo.ajax(
5150 $link.attr('href'),
5151 { _xfConfirm: 1 },
5152 function (ajaxData, textStatus) {
5153 if (XenForo.hasResponseError(ajaxData)) {
5154 return false;
5155 }
5156
5157 $link.xfFadeOut(XenForo.speed.fast, function () {
5158 $link
5159 .attr('href', ajaxData.linkUrl)
5160 .html(ajaxData.linkPhrase)
5161 .xfFadeIn(XenForo.speed.fast);
5162 });
5163 }
5164 );
5165 });
5166 };
5167
5168 // *********************************************************************
5169
5170 /**
5171 * Allows relative hash links to smoothly scroll into place,
5172 * Primarily used for 'x posted...' messages on bb code quote.
5173 *
5174 * @param jQuery a.AttributionLink
5175 */
5176 XenForo.AttributionLink = function ($link) {
5177 $link.click(function (e) {
5178 if ($(this.hash).length) {
5179 try {
5180 var hash = this.hash,
5181 top = $(this.hash).offset().top,
5182 scroller = XenForo.getPageScrollTagName();
5183
5184 if ("pushState" in window.history) {
5185 window.history.pushState({}, '', window.location.toString().replace(/#.*$/, '') + hash);
5186 }
5187
5188 var menu_height = $('#navigation').get(0).getBoundingClientRect().height || 45;
5189
5190 $(scroller).animate({ scrollTop: top - menu_height }, XenForo.speed.normal, 'easeOutBack', function () {
5191 if (!window.history.pushState) {
5192 window.location.hash = hash;
5193 }
5194 XenForo.animateBackgroundColor($(hash));
5195 });
5196 } catch (e) {
5197 window.location.hash = this.hash;
5198 }
5199
5200 e.preventDefault();
5201 }
5202 });
5203 };
5204
5205 // *********************************************************************
5206
5207 /**
5208 * Allows clicks on one element to trigger the click event of another
5209 *
5210 * @param jQuery .ClickProxy[rel="{selectorForTarget}"]
5211 *
5212 * @return boolean false - prevents any direct action for the proxy element on click
5213 */
5214 XenForo.ClickProxy = function ($element) {
5215 $element.click(function (e) {
5216 $($element.attr('rel')).click();
5217
5218 if (!$element.data('allowdefault')) {
5219 return false;
5220 }
5221 });
5222 };
5223
5224 // *********************************************************************
5225
5226 /**
5227 * ReCaptcha wrapper
5228 */
5229 XenForo.ReCaptcha = function ($captcha) {
5230 this.__construct($captcha);
5231 };
5232 XenForo.ReCaptcha.prototype =
5233 {
5234 __construct: function ($captcha) {
5235 if (XenForo.ReCaptcha.instance) {
5236 XenForo.ReCaptcha.instance.remove();
5237 }
5238 XenForo.ReCaptcha.instance = this;
5239
5240 this.publicKey = $captcha.data('publickey');
5241 if (!this.publicKey) {
5242 return;
5243 }
5244
5245 $captcha.siblings('noscript').remove();
5246
5247 $captcha.uniqueId();
5248 this.$captcha = $captcha;
5249 this.type = 'image';
5250
5251 $captcha.find('.ReCaptchaReload').click($.context(this, 'reload'));
5252 $captcha.find('.ReCaptchaSwitch').click($.context(this, 'switchType'));
5253
5254 this.load();
5255
5256 $captcha.closest('form.AutoValidator').bind(
5257 {
5258 AutoValidationDataReceived: $.context(this, 'reload')
5259 });
5260 },
5261
5262 load: function () {
5263 if (window.Recaptcha) {
5264 this.create();
5265 } else {
5266 var f = $.context(this, 'create'),
5267 delay = ($.browser.msie && $.browser.version <= 6 ? 250 : 0); // helps IE6 loading
5268
5269 $.getScript('https://www.google.com/recaptcha/api/js/recaptcha_ajax.js',
5270 function () {
5271 setTimeout(f, delay);
5272 }
5273 );
5274 }
5275 },
5276
5277 create: function () {
5278 var $c = this.$captcha;
5279
5280 window.Recaptcha.create(this.publicKey, $c.attr('id'),
5281 {
5282 theme: 'custom',
5283 callback: function () {
5284 $c.show();
5285 $('#ReCaptchaLoading').remove();
5286 // webkit seems to overwrite this value using the back button
5287 $('#recaptcha_challenge_field').val(window.Recaptcha.get_challenge());
5288 }
5289 });
5290 },
5291
5292 reload: function (e) {
5293 if (!window.Recaptcha) {
5294 return;
5295 }
5296
5297 if (!$(e.target).is('form')) {
5298 e.preventDefault();
5299 }
5300 window.Recaptcha.reload();
5301 },
5302
5303 switchType: function (e) {
5304 e.preventDefault();
5305 this.type = (this.type == 'image' ? 'audio' : 'image');
5306 window.Recaptcha.switch_type(this.type);
5307 },
5308
5309 remove: function () {
5310 this.$captcha.empty().remove();
5311 if (window.Recaptcha) {
5312 window.Recaptcha.destroy();
5313 }
5314 }
5315 };
5316 XenForo.ReCaptcha.instance = null;
5317
5318 XenForo.NoCaptcha = function ($captcha) {
5319 this.__construct($captcha);
5320 };
5321 XenForo.NoCaptcha.prototype =
5322 {
5323 __construct: function ($captcha) {
5324 this.$captcha = $captcha;
5325 this.noCaptchaId = null;
5326
5327 $captcha.closest('form.AutoValidator').bind(
5328 {
5329 AutoValidationDataReceived: $.context(this, 'reload')
5330 });
5331
5332 if (window.grecaptcha) {
5333 this.create();
5334 } else {
5335 XenForo.NoCaptcha._callbacks.push($.context(this, 'create'));
5336 $.getScript('https://www.google.com/recaptcha/api.js?onload=XFNoCaptchaCallback&render=explicit');
5337 }
5338 },
5339
5340 create: function () {
5341 if (!window.grecaptcha) {
5342 return;
5343 }
5344
5345 this.noCaptchaId = grecaptcha.render(this.$captcha[0], { sitekey: this.$captcha.data('sitekey') });
5346 },
5347
5348 reload: function () {
5349 if (!window.grecaptcha || this.noCaptchaId === null) {
5350 return;
5351 }
5352
5353 grecaptcha.reset(this.noCaptchaId);
5354 }
5355 };
5356 XenForo.NoCaptcha._callbacks = [];
5357 window.XFNoCaptchaCallback = function () {
5358 var cb = XenForo.NoCaptcha._callbacks;
5359
5360 for (var i = 0; i < cb.length; i++) {
5361 cb[i]();
5362 }
5363 };
5364
5365 // *********************************************************************
5366
5367 XenForo.SolveMediaCaptcha = function ($captcha) {
5368 this.__construct($captcha);
5369 };
5370 XenForo.SolveMediaCaptcha.prototype =
5371 {
5372 __construct: function ($captcha) {
5373 if (XenForo.SolveMediaCaptcha.instance) {
5374 XenForo.SolveMediaCaptcha.instance.remove();
5375 }
5376 XenForo.SolveMediaCaptcha.instance = this;
5377
5378 this.cKey = $captcha.data('c-key');
5379 if (!this.cKey) {
5380 return;
5381 }
5382
5383 $captcha.siblings('noscript').remove();
5384
5385 $captcha.uniqueId();
5386 this.$captcha = $captcha;
5387 this.type = 'image';
5388
5389 this.load();
5390
5391 $captcha.closest('form.AutoValidator').bind(
5392 {
5393 AutoValidationDataReceived: $.context(this, 'reload')
5394 });
5395 },
5396
5397 load: function () {
5398 if (window.ACPuzzle) {
5399 this.create();
5400 } else {
5401 var prefix = window.location.protocol == 'https:' ? 'https://api-secure' : 'http://api';
5402
5403 window.ACPuzzleOptions = {
5404 onload: $.context(this, 'create')
5405 };
5406 XenForo.loadJs(prefix + '.solvemedia.com/papi/challenge.ajax');
5407 }
5408 },
5409
5410 create: function () {
5411 var $c = this.$captcha;
5412
5413 window.ACPuzzle.create(this.cKey, $c.attr('id'), {
5414 theme: $c.data('theme') || 'white',
5415 lang: $('html').attr('lang').substr(0, 2) || 'en'
5416 });
5417 },
5418
5419 reload: function (e) {
5420 if (!window.ACPuzzle) {
5421 return;
5422 }
5423
5424 if (!$(e.target).is('form')) {
5425 e.preventDefault();
5426 }
5427 window.ACPuzzle.reload();
5428 },
5429
5430 remove: function () {
5431 this.$captcha.empty().remove();
5432 if (window.ACPuzzle) {
5433 window.ACPuzzle.destroy();
5434 }
5435 }
5436 };
5437 XenForo.SolveMediaCaptcha.instance = null;
5438
5439 // *********************************************************************
5440
5441 XenForo.KeyCaptcha = function ($captcha) {
5442 this.__construct($captcha);
5443 };
5444 XenForo.KeyCaptcha.prototype =
5445 {
5446 __construct: function ($captcha) {
5447 this.$captcha = $captcha;
5448
5449 this.$form = $captcha.closest('form');
5450 this.$form.uniqueId();
5451
5452 this.$codeEl = this.$form.find('input[name=keycaptcha_code]');
5453 this.$codeEl.uniqueId();
5454
5455 this.load();
5456 $captcha.closest('form.AutoValidator').bind({
5457 AutoValidationDataReceived: $.context(this, 'reload')
5458 });
5459 },
5460
5461 load: function () {
5462 if (window.s_s_c_onload) {
5463 this.create();
5464 } else {
5465 var $captcha = this.$captcha;
5466
5467 window.s_s_c_user_id = $captcha.data('user-id');
5468 window.s_s_c_session_id = $captcha.data('session-id');
5469 window.s_s_c_captcha_field_id = this.$codeEl.attr('id');
5470 window.s_s_c_submit_button_id = 'sbutton-#-r';
5471 window.s_s_c_web_server_sign = $captcha.data('sign');
5472 window.s_s_c_web_server_sign2 = $captcha.data('sign2');
5473 document.s_s_c_element = this.$form[0];
5474 document.s_s_c_debugmode = 1;
5475
5476 var $div = $('#div_for_keycaptcha');
5477 if (!$div.length) {
5478 $('body').append('<div id="div_for_keycaptcha" />');
5479 }
5480
5481 XenForo.loadJs('https://backs.keycaptcha.com/swfs/cap.js');
5482 }
5483 },
5484
5485 create: function () {
5486 window.s_s_c_onload(this.$form.attr('id'), this.$codeEl.attr('id'), 'sbutton-#-r');
5487 },
5488
5489 reload: function (e) {
5490 if (!window.s_s_c_onload) {
5491 return;
5492 }
5493
5494 if (!$(e.target).is('form')) {
5495 e.preventDefault();
5496 }
5497 this.load();
5498 }
5499 };
5500
5501 // *********************************************************************
5502
5503 /**
5504 * Loads a new (non-ReCaptcha) CAPTCHA upon verification failure
5505 *
5506 * @param jQuery #Captcha
5507 */
5508 XenForo.Captcha = function ($container) {
5509 var $form = $container.closest('form');
5510
5511 $form.off('AutoValidationDataReceived.captcha').on('AutoValidationDataReceived.captcha', function (e) {
5512 $container.fadeTo(XenForo.speed.fast, 0.5);
5513
5514 XenForo.ajax($container.data('source'), {}, function (ajaxData, textStatus) {
5515 if (XenForo.hasResponseError(ajaxData)) {
5516 return false;
5517 }
5518
5519 if (XenForo.hasTemplateHtml(ajaxData)) {
5520 $container.xfFadeOut(XenForo.speed.xfast, function () {
5521 $(ajaxData.templateHtml).xfInsert('replaceAll', $container, 'xfFadeIn', XenForo.speed.xfast);
5522 });
5523 }
5524 });
5525 });
5526 };
5527
5528 // *********************************************************************
5529
5530 /**
5531 * Handles resizing of BB code [img] tags that would overflow the page
5532 *
5533 * @param jQuery img.bbCodeImage
5534 */
5535 XenForo.BbCodeImage = function ($image) {
5536 this.__construct($image);
5537 };
5538 XenForo.BbCodeImage.prototype =
5539 {
5540 __construct: function ($image) {
5541 this.$image = $image;
5542 this.actualWidth = 0;
5543
5544 if ($image.closest('a').length) {
5545 return;
5546 }
5547
5548 $image
5549 .attr('title', XenForo.phrases.click_image_show_full_size_version || 'Show full size')
5550 .click($.context(this, 'toggleFullSize'));
5551
5552 if (!XenForo.isTouchBrowser()) {
5553 this.$image.tooltip(XenForo.configureTooltipRtl({
5554 effect: 'slide',
5555 slideOffset: 30,
5556 position: 'top center',
5557 offset: [45, 0],
5558 tipClass: 'xenTooltip bbCodeImageTip',
5559 onBeforeShow: $.context(this, 'isResized'),
5560 onShow: $.context(this, 'addTipClick')
5561 }));
5562 }
5563
5564 if (!this.getImageWidth()) {
5565 var src = $image.attr('src');
5566
5567 $image.bind({
5568 load: $.context(this, 'getImageWidth')
5569 });
5570 //$image.attr('src', 'about:blank');
5571 $image.attr('src', src);
5572 }
5573 },
5574
5575 /**
5576 * Attempts to store the un-resized width of the image
5577 *
5578 * @return integer
5579 */
5580 getImageWidth: function () {
5581 this.$image.css({ 'max-width': 'none', 'max-height': 'none' });
5582 this.actualWidth = this.$image.width();
5583 this.$image.css({ 'max-width': '', 'max-height': '' });
5584
5585 //console.log('BB Code Image %o has width %s', this.$image, this.actualWidth);
5586
5587 return this.actualWidth;
5588 },
5589
5590 /**
5591 * Shows and hides a full-size version of the image
5592 *
5593 * @param event
5594 */
5595 toggleFullSize: function (e) {
5596 if (this.actualWidth == 0) {
5597 this.getImageWidth();
5598 }
5599
5600 var currentWidth = this.$image.width(),
5601 offset, cssOffset, scale,
5602 scrollLeft, scrollTop,
5603 layerX, layerY,
5604 $fullSizeImage,
5605 speed = window.navigator.userAgent.match(/Android|iOS|iPhone|iPad|Mobile Safari/i) ? 0 : XenForo.speed.normal,
5606 easing = 'easeInOutQuart';
5607
5608 if (this.actualWidth > currentWidth) {
5609 offset = this.$image.offset();
5610 cssOffset = offset;
5611 scale = this.actualWidth / currentWidth;
5612 layerX = e.pageX - offset.left;
5613 layerY = e.pageY - offset.top;
5614
5615 if (XenForo.isRTL()) {
5616 cssOffset.right = $('html').width() - cssOffset.left - currentWidth;
5617 cssOffset.left = 'auto';
5618 }
5619
5620 $fullSizeImage = $('<img />', { src: this.$image.attr('src') })
5621 .addClass('bbCodeImageFullSize')
5622 .css('width', currentWidth)
5623 .css(cssOffset)
5624 .click(function () {
5625 $(this).remove();
5626 $(XenForo.getPageScrollTagName()).scrollLeft(0).scrollTop(offset.top);
5627 })
5628 .appendTo('body')
5629 .animate({ width: this.actualWidth }, speed, easing);
5630
5631 // remove full size image if an overlay is about to open
5632 $(document).one('OverlayOpening', function () {
5633 $fullSizeImage.remove();
5634 });
5635
5636 // remove full-size image if the source image is contained by a ToggleTrigger target that is closing
5637 $(document).bind('ToggleTriggerEvent', $.context(function (e, args) {
5638 if (args.closing && args.$target.find(this.$image).length) {
5639 console.info('Target is parent of this image %o', this.$image);
5640 $fullSizeImage.remove();
5641 }
5642 }, this));
5643
5644 if (e.target == this.$image.get(0)) {
5645 scrollLeft = offset.left + (e.pageX - offset.left) * scale - $(window).width() / 2;
5646 scrollTop = offset.top + (e.pageY - offset.top) * scale - $(window).height() / 2;
5647 } else {
5648 scrollLeft = offset.left + (this.actualWidth / 2) - $(window).width() / 2;
5649 scrollTop = offset.top + (this.$image.height() * scale / 2) - $(window).height() / 2;
5650 }
5651
5652 $(XenForo.getPageScrollTagName()).animate(
5653 {
5654 scrollLeft: scrollLeft,
5655 scrollTop: scrollTop
5656 }, speed, easing, $.context(function () {
5657 var tooltip = this.$image.data('tooltip');
5658 if (tooltip) {
5659 tooltip.hide();
5660 }
5661 }, this));
5662 } else {
5663 console.log('BBCodeImage: this.actualWidth = %d, currentWidth = %d', this.actualWidth, currentWidth);
5664 }
5665 },
5666
5667 isResized: function (e) {
5668 var width = this.$image.width();
5669
5670 if (!width) {
5671 return false;
5672 }
5673
5674 if (this.getImageWidth() <= width) {
5675 //console.log('Image is not resized %o', this.$image);
5676 return false;
5677 }
5678 },
5679
5680 addTipClick: function (e) {
5681 if (!this.tipClickAdded) {
5682 $(this.$image.data('tooltip').getTip()).click($.context(this, 'toggleFullSize'));
5683 this.tipClickAdded = true;
5684 }
5685 }
5686 };
5687
5688 // *********************************************************************
5689
5690 /**
5691 * Wrapper for the jQuery Tools Tabs system
5692 *
5693 * @param jQuery .Tabs
5694 */
5695 XenForo.Tabs = function ($tabContainer) {
5696 this.__construct($tabContainer);
5697 };
5698 XenForo.Tabs.prototype =
5699 {
5700 __construct: function ($tabContainer) {
5701 // var useHistory = XenForo.isPositive($tabContainer.data('history'));
5702 // TODO: disabled until base tag issues are resolved
5703 var useHistory = false;
5704
5705 this.$tabContainer = $tabContainer;
5706 this.$panes = $($tabContainer.data('panes'));
5707
5708 /*if (useHistory)
5709 {
5710 $tabContainer.find('a[href]').each(function()
5711 {
5712 var $this = $(this), hrefParts = $this.attr('href').split('#');
5713 if (hrefParts[1] && location.pathname == hrefParts[0])
5714 {
5715 $this.attr('href', '#' + hrefParts[1]);
5716 }
5717 });
5718 }*/
5719
5720 var $tabs = $tabContainer.find('a');
5721 if (!$tabs.length) {
5722 $tabs = $tabContainer.children();
5723 }
5724
5725 var $active = $tabs.filter('.active'),
5726 initialIndex = 0;
5727
5728 if ($active.length) {
5729 $tabs.each(function () {
5730 if (this == $active.get(0)) {
5731 return false;
5732 }
5733
5734 initialIndex++;
5735 });
5736 }
5737
5738 if (window.location.hash.length > 1) {
5739 var id = window.location.hash.substr(1),
5740 matchIndex = -1,
5741 matched = false;
5742
5743 this.$panes.each(function () {
5744 matchIndex++;
5745 if ($(this).attr('id') === id) {
5746 matched = true;
5747 return false;
5748 }
5749 return true;
5750 });
5751 if (matched) {
5752 initialIndex = matchIndex;
5753 }
5754 }
5755
5756 $tabContainer.tabs(this.$panes, {
5757 current: 'active',
5758 history: useHistory,
5759 initialIndex: initialIndex,
5760 onBeforeClick: $.context(this, 'onBeforeClick')
5761 });
5762 this.api = $tabContainer.data('tabs');
5763 this.useInfiniteScroll = !!this.$panes.parents('#AlertPanels').length
5764 },
5765
5766 getCurrentTab: function () {
5767 return this.api.getIndex();
5768 },
5769
5770 click: function (index) {
5771 this.api.click(index);
5772 },
5773
5774 onBeforeClick: function (e, index) {
5775 var self = this
5776 this.$tabContainer.children().each(function (i) {
5777 if (index == i) {
5778 $(this).addClass('active');
5779 } else {
5780 $(this).removeClass('active');
5781 }
5782 });
5783
5784 var $pane = $(this.$panes.get(index)),
5785 loadUrl = $pane.data('loadurl');
5786
5787 if (loadUrl) {
5788 $pane.data('loadurl', '');
5789
5790 XenForo.ajax(loadUrl, {}, function (ajaxData) {
5791 if (XenForo.hasTemplateHtml(ajaxData) || XenForo.hasTemplateHtml(ajaxData, 'message')) {
5792 new XenForo.ExtLoader(ajaxData, function (ajaxData) {
5793 var $html;
5794
5795 if (ajaxData.templateHtml) {
5796 $html = $(ajaxData.templateHtml);
5797 } else if (ajaxData.message) {
5798 $html = $('<div class="section" />').html(ajaxData.message);
5799 }
5800
5801 $pane.html('');
5802
5803 if ($html) {
5804 $html.xfInsert('appendTo', $pane, 'xfFadeIn', 0);
5805 }
5806
5807 if ($pane.hasClass('Scrollbar')) {
5808 $pane.scrollbar();
5809 }
5810
5811 if ($html.length > 1) {
5812 $pane.parent().xfActivate()
5813 }
5814
5815 if ($pane.find('form.InlineModForm').length) {
5816 XenForo.create('XenForo.InlineModForm', $pane.find('form.InlineModForm'));
5817 }
5818
5819 if (self.useInfiniteScroll) {
5820 var $status = $('<div class="infinite-scroll-status">\
5821 <div class="spinner infinite-scroll-request" style="display: none;">\
5822 <div class="bounce1"></div>\
5823 <div class="bounce2"></div>\
5824 <div class="bounce3"></div>\
5825 </div>\
5826 </div>').appendTo($pane)
5827 $pane.find('.alertsPopup > ol').infiniteScroll({
5828 path: function () {
5829 return XenForo.canonicalizeUrl(loadUrl + (/\?/.test(loadUrl) ? '&' : '?') + 'page=' + (this.pageIndex + 1))
5830 },
5831 status: $status[0],
5832 append: '.alertsPopup > ol > li',
5833 xf: true,
5834 xfAjaxOptions: { global: false },
5835 xfAjaxDataProcess: function (ajaxData) {
5836 return ajaxData.templateHtml
5837 },
5838 history: false,
5839 elementScroll: $pane[0]
5840 })
5841 }
5842 });
5843 } else if (XenForo.hasResponseError(ajaxData)) {
5844 return false;
5845 }
5846 }, { type: 'GET' });
5847 }
5848 }
5849 };
5850
5851 // *********************************************************************
5852
5853 /**
5854 * Handles a like / unlike link being clicked
5855 *
5856 * @param jQuery a.LikeLink
5857 */
5858 XenForo.LikeLink = function ($link) {
5859 $link.click(function (e) {
5860 e.preventDefault();
5861
5862 var $link = $(this);
5863
5864 XenForo.ajax(this.href, {}, function (ajaxData, textStatus) {
5865 if (XenForo.hasResponseError(ajaxData)) {
5866 return false;
5867 }
5868
5869 $link.stop(true, true);
5870
5871 if (ajaxData.term) // term = Like / Unlike
5872 {
5873 $link.find('.LikeLabel').html(ajaxData.term);
5874
5875 if (ajaxData.cssClasses) {
5876 $.each(ajaxData.cssClasses, function (className, action) {
5877 $link[action === '+' ? 'addClass' : 'removeClass'](className);
5878 });
5879 }
5880 }
5881
5882 const tooltip = $link[0]._tippy;
5883 console.log(tooltip);
5884
5885 if (ajaxData.templateHtml === '') {
5886 if (tooltip) {
5887 tooltip.hide()
5888 setTimeout(() => {
5889 $link.data('XenForo.Tooltip', null);
5890 }, 275)
5891 }
5892 }
5893 else {
5894 if (tooltip) {
5895 tooltip.setContent(ajaxData.templateHtml);
5896 $('.tippy-content').xfActivate();
5897 }
5898 else {
5899 $link.addClass('Tooltip PopupTooltip');
5900 $('<div>').addClass('TooltipContent').html(ajaxData.templateHtml).insertAfter($link);
5901 $link.parent().xfActivate();
5902 $link[0]._tippy.show();
5903 $link[0]._tippy.setContent(ajaxData.templateHtml);
5904 $('.tippy-content').xfActivate();
5905 }
5906 }
5907
5908
5909 /*var id = $link.data('container').substr(1);
5910 $(document.createElement('div')).attr('id', id).html(ajaxData.templateHtml).hide().appendTo('body');
5911 $link.data('XenForo.Tooltip', null);
5912 XenForo.register($link, 'XenForo.Tooltip');
5913 console.log($link.data());*/
5914
5915 /*if (ajaxData.templateHtml === '') {
5916 $($link.data('container')).xfFadeUp(XenForo.speed.fast, function () {
5917 $(this).empty().xfFadeDown(0);
5918 });
5919 } else {
5920 var $container = $($link.data('container')),
5921 $likeText = $container.find('.LikeText'),
5922 $templateHtml = $(ajaxData.templateHtml);
5923
5924 if ($likeText.length) {
5925 // we already have the likes_summary template in place, so just replace the text
5926 $likeText.xfFadeOut(50, function () {
5927 var textContainer = this.parentNode;
5928
5929 $(this).remove();
5930
5931 $templateHtml.find('.LikeText').xfInsert('appendTo', textContainer, 'xfFadeIn', 50);
5932 });
5933 } else {
5934 new XenForo.ExtLoader(ajaxData, function () {
5935 $templateHtml.xfInsert('appendTo', $container);
5936 });
5937 }
5938 }*/
5939 });
5940 });
5941 };
5942
5943 // *********************************************************************
5944
5945 XenForo.Facebook =
5946 {
5947 initialized: false,
5948 loading: false,
5949 parsed: false,
5950 appId: '',
5951 fbUid: 0,
5952 authResponse: {},
5953 locale: 'en-US',
5954
5955 init: function () {
5956 if (XenForo.Facebook.initialized) {
5957 return;
5958 }
5959 XenForo.Facebook.initialized = true;
5960 XenForo.Facebook.loading = false;
5961
5962 $(document.body).append($('<div id="fb-root" />'));
5963
5964 var fbInfo = {
5965 version: 'v2.10',
5966 xfbml: false,
5967 oauth: true,
5968 channelUrl: XenForo.canonicalizeUrl('fb_channel.php?l=' + XenForo.Facebook.locale)
5969 };
5970 if (XenForo.Facebook.appId) {
5971 fbInfo.appId = XenForo.Facebook.appId;
5972 }
5973
5974 FB.init(fbInfo);
5975 if (XenForo.Facebook.appId && XenForo.Facebook.fbUid) {
5976 FB.Event.subscribe('auth.authResponseChange', XenForo.Facebook.sessionChange);
5977 FB.getLoginStatus(XenForo.Facebook.sessionChange);
5978
5979 if (XenForo.visitor.user_id) {
5980 $(document).delegate('a.LogOut:not(.OverlayTrigger)', 'click', XenForo.Facebook.eLogOutClick);
5981 }
5982 }
5983
5984 FB.XFBML.parse(null, function () {
5985 XenForo.Facebook.parsed = true;
5986 });
5987 },
5988
5989 start: function () {
5990 var cookieUid = $.getCookie('fbUid');
5991 if (cookieUid && cookieUid.length) {
5992 XenForo.Facebook.fbUid = parseInt(cookieUid, 10);
5993 }
5994
5995 if ($('.fb-post, .fb-video, .fb-page, .fb-like, .fb-share-button').length) {
5996 XenForo.Facebook.forceInit = true;
5997 }
5998
5999 if (!XenForo.Facebook.forceInit && (!XenForo.Facebook.appId || !XenForo.Facebook.fbUid)) {
6000 return;
6001 }
6002
6003 XenForo.Facebook.load();
6004 },
6005
6006 load: function () {
6007 if (XenForo.Facebook.parsed) {
6008 FB.XFBML.parse();
6009 return;
6010 }
6011
6012 if (XenForo.Facebook.loading) {
6013 return;
6014 }
6015 XenForo.Facebook.loading = true;
6016
6017 XenForo.Facebook.locale = $('html').attr('lang').replace('-', '_');
6018 if (!XenForo.Facebook.locale) {
6019 XenForo.Facebook.locale = 'en_US';
6020 }
6021
6022 var e = document.createElement('script'),
6023 locale = XenForo.Facebook.locale.replace('-', '_');
6024 e.src = 'https://connect.facebook.net/' + XenForo.Facebook.locale + '/sdk.js';
6025 e.async = true;
6026
6027 window.fbAsyncInit = XenForo.Facebook.init;
6028 document.getElementsByTagName('head')[0].appendChild(e);
6029 },
6030
6031 sessionChange: function (response) {
6032 if (!XenForo.Facebook.fbUid) {
6033 return;
6034 }
6035
6036 var authResponse = response.authResponse, visitor = XenForo.visitor;
6037 XenForo.Facebook.authResponse = authResponse;
6038
6039 if (authResponse && !visitor.user_id) {
6040 if (!XenForo._noSocialLogin) {
6041 // facebook user, connect!
6042 XenForo.alert(XenForo.phrases.logging_in + '...', '', 8000);
6043 setTimeout(function () {
6044 XenForo.redirect(
6045 'register/facebook&t=' + escape(authResponse.accessToken)
6046 + '&redirect=' + escape(window.location)
6047 );
6048 }, 250);
6049 }
6050 }
6051 },
6052
6053 logout: function (fbData, returnPage) {
6054 var location = $('a.LogOut:not(.OverlayTrigger)').attr('href');
6055 if (!location) {
6056 location = 'logout/&_xfToken=' + XenForo._csrfToken;
6057 }
6058 if (returnPage) {
6059 location += (location.indexOf('?') >= 0 ? '&' : '?') + 'redirect=' + escape(window.location);
6060 }
6061 XenForo.redirect(location);
6062 },
6063
6064 eLogOutClick: function (e) {
6065 if (XenForo.Facebook.authResponse && XenForo.Facebook.authResponse.userID) {
6066 FB.logout(XenForo.Facebook.logout);
6067 return false;
6068 }
6069 }
6070 };
6071
6072 // *********************************************************************
6073 /**
6074 * Turns an :input into a Prompt
6075 *
6076 * @param {Object} :input[placeholder]
6077 */
6078 XenForo.Prompt = function ($input) {
6079 this.__construct($input);
6080 };
6081 if ('placeholder' in document.createElement('input')) {
6082 // native placeholder support
6083 XenForo.Prompt.prototype =
6084 {
6085 __construct: function ($input) {
6086 this.$input = $input;
6087 },
6088
6089 isEmpty: function () {
6090 return (this.$input.strval() === '');
6091 },
6092
6093 val: function (value, focus) {
6094 if (value === undefined) {
6095 return this.$input.val();
6096 } else {
6097 if (focus) {
6098 this.$input.focus();
6099 }
6100
6101 return this.$input.val(value);
6102 }
6103 }
6104 };
6105 } else {
6106 // emulate placeholder support
6107 XenForo.Prompt.prototype =
6108 {
6109 __construct: function ($input) {
6110 console.log('Emulating placeholder behaviour for %o', $input);
6111
6112 this.placeholder = $input.attr('placeholder');
6113
6114 this.$input = $input.bind(
6115 {
6116 focus: $.context(this, 'setValueMode'),
6117 blur: $.context(this, 'setPromptMode')
6118 });
6119
6120 this.$input.closest('form').bind(
6121 {
6122 submit: $.context(this, 'eFormSubmit'),
6123 AutoValidationBeforeSubmit: $.context(this, 'eFormSubmit'),
6124 AutoValidationComplete: $.context(this, 'eFormSubmitted')
6125 });
6126
6127 this.setPromptMode();
6128 },
6129
6130 /**
6131 * If the prompt box contains no text, or contains the prompt text (only) it is 'empty'
6132 *
6133 * @return boolean
6134 */
6135 isEmpty: function () {
6136 var val = this.$input.val();
6137
6138 return (val === '' || val == this.placeholder);
6139 },
6140
6141 /**
6142 * When exiting the prompt box, update its contents if necessary
6143 */
6144 setPromptMode: function () {
6145 if (this.isEmpty()) {
6146 this.$input.val(this.placeholder).addClass('prompt');
6147 }
6148 },
6149
6150 /**
6151 * When entering the prompt box, clear its contents if it is 'empty'
6152 */
6153 setValueMode: function () {
6154 if (this.isEmpty()) {
6155 this.$input.val('').removeClass('prompt').select();
6156 }
6157 },
6158
6159 /**
6160 * Gets or sets the value of the prompt and puts it into the correct mode for its contents
6161 *
6162 * @param string value
6163 */
6164 val: function (value, focus) {
6165 // get value
6166 if (value === undefined) {
6167 if (this.isEmpty()) {
6168 return '';
6169 } else {
6170 return this.$input.val();
6171 }
6172 }
6173
6174 // clear value
6175 else if (value === '') {
6176 this.$input.val('');
6177
6178 if (focus === undefined) {
6179 this.setPromptMode();
6180 }
6181 }
6182
6183 // set value
6184 else {
6185 this.setValueMode();
6186 this.$input.val(value);
6187 }
6188 },
6189
6190 /**
6191 * When the form is submitted, empty the prompt box if it is 'empty'
6192 *
6193 * @return boolean true;
6194 */
6195 eFormSubmit: function () {
6196 if (this.isEmpty()) {
6197 this.$input.val('');
6198 }
6199
6200 return true;
6201 },
6202
6203 /**
6204 * Fires immediately after the form has sent its AJAX submission
6205 */
6206 eFormSubmitted: function () {
6207 this.setPromptMode();
6208 }
6209 };
6210 }
6211 ;
6212
6213 // *********************************************************************
6214
6215 /**
6216 * Turn in input:text.SpinBox into a Spin Box
6217 * Requires a parameter class of 'SpinBox' and an attribute of 'data-step' with a numeric step value.
6218 * data-max and data-min parameters are optional.
6219 *
6220 * @param {Object} $input
6221 */
6222 XenForo.SpinBox = function ($input) {
6223 this.__construct($input);
6224 };
6225 XenForo.SpinBox.prototype =
6226 {
6227 __construct: function ($input) {
6228 var param,
6229 inputWidth,
6230 $plusButton,
6231 $minusButton;
6232
6233 if ($input.attr('step') === undefined) {
6234 console.warn('ERROR: No data-step attribute specified for spinbox.');
6235 return;
6236 }
6237
6238 this.parameters = { step: null, min: null, max: null };
6239
6240 for (param in this.parameters) {
6241 if ($input.attr(param) === undefined) {
6242 delete this.parameters[param];
6243 } else {
6244 this.parameters[param] = parseFloat($input.attr(param));
6245 }
6246 }
6247
6248 inputWidth = $input.width();
6249
6250 $plusButton = $('<input type="button" class="button spinBoxButton up" value="+" data-plusminus="+" tabindex="-1" />')
6251 .insertAfter($input)
6252 .focus($.context(this, 'eFocusButton'))
6253 .click($.context(this, 'eClickButton'))
6254 .mouseenter($.context(this, 'eMouseEnter'))
6255 .mousedown($.context(this, 'eMousedownButton'))
6256 .on('mouseleave mouseup', $.context(this, 'eMouseupButton'));
6257 $minusButton = $('<input type="button" class="button spinBoxButton down" value="-" data-plusminus="-" tabindex="-1" />')
6258 .insertAfter($plusButton)
6259 .focus($.context(this, 'eFocusButton'))
6260 .click($.context(this, 'eClickButton'))
6261 .mouseenter($.context(this, 'eMouseEnter'))
6262 .mousedown($.context(this, 'eMousedownButton'))
6263 .on('mouseleave mouseup', $.context(this, 'eMouseupButton'));
6264
6265 // set up the input
6266 this.$input = $input
6267 .attr('autocomplete', 'off')
6268 .blur($.context(this, 'eBlurInput'))
6269 .keyup($.context(this, 'eKeyupInput'));
6270
6271 // force validation to occur on form submit
6272 this.$input.closest('form').bind('submit', $.context(this, 'eBlurInput'));
6273
6274 // initial constraint
6275 this.$input.val(this.constrain(this.getValue()));
6276
6277 this.mouseTarget = null;
6278 },
6279
6280 /**
6281 * Returns the (numeric) value of the spinbox
6282 *
6283 * @return float
6284 */
6285 getValue: function () {
6286 var value = parseFloat(this.$input.val());
6287
6288 value = (isNaN(value)) ? parseFloat(this.$input.val().replace(/[^0-9.]/g, '')) : value;
6289
6290 return (isNaN(value) ? 0 : value);
6291 },
6292
6293 /**
6294 * Asserts that the value of the spinbox is within defined min and max parameters.
6295 *
6296 * @param float Spinbox value
6297 *
6298 * @return float
6299 */
6300 constrain: function (value) {
6301 if (this.parameters.min !== undefined && value < this.parameters.min) {
6302 console.warn('Minimum value for SpinBox = %s\n %o', this.parameters.min, this.$input);
6303 return this.parameters.min;
6304 } else if (this.parameters.max !== undefined && value > this.parameters.max) {
6305 console.warn('Maximum value for SpinBox = %s\n %o', this.parameters.max, this.$input);
6306 return this.parameters.max;
6307 } else {
6308 return value;
6309 }
6310 },
6311
6312 /**
6313 * Takes the value of the SpinBox input to the nearest step.
6314 *
6315 * @param string +/- Take the value up or down
6316 */
6317 stepValue: function (plusMinus) {
6318 if (this.$input.prop('readonly')) {
6319 return false;
6320 }
6321
6322 var val = this.getValue(),
6323 mod = val % this.parameters.step,
6324 posStep = (plusMinus == '+'),
6325 newVal = val - mod;
6326
6327 if (!mod || (posStep && mod > 0) || (!posStep && mod < 0)) {
6328 newVal = newVal + this.parameters.step * (posStep ? 1 : -1);
6329 }
6330
6331 this.$input.val(this.constrain(newVal));
6332 this.$input.triggerHandler('change');
6333 },
6334
6335 /**
6336 * Handles the input being blurred. Removes the 'pseudofocus' class and constrains the spinbox value.
6337 *
6338 * @param Event e
6339 */
6340 eBlurInput: function (e) {
6341 this.$input.val(this.constrain(this.getValue()));
6342 },
6343
6344 /**
6345 * Handles key events on the spinbox input. Up and down arrows perform a value step.
6346 *
6347 * @param Event e
6348 *
6349 * @return false|undefined
6350 */
6351 eKeyupInput: function (e) {
6352 switch (e.which) {
6353 case 38: // up
6354 {
6355 this.stepValue('+');
6356 this.$input.select();
6357 return false;
6358 }
6359
6360 case 40: // down
6361 {
6362 this.stepValue('-');
6363 this.$input.select();
6364 return false;
6365 }
6366 }
6367 },
6368
6369 /**
6370 * Handles focus events on spinbox buttons.
6371 *
6372 * Does not allow buttons to keep focus, returns focus to the input.
6373 *
6374 * @param Event e
6375 *
6376 * @return boolean false
6377 */
6378 eFocusButton: function (e) {
6379 return false;
6380 },
6381
6382 /**
6383 * Handles click events on spinbox buttons.
6384 *
6385 * The buttons are assumed to have data-plusMinus attributes of + or -
6386 *
6387 * @param Event e
6388 */
6389 eClickButton: function (e) {
6390 this.stepValue($(e.target).data('plusminus'));
6391 this.$input.focus();
6392 this.$input.select();
6393 },
6394
6395 /**
6396 * Handles a mouse-down event on a spinbox button in order to allow rapid repeats.
6397 *
6398 * @param Event e
6399 */
6400 eMousedownButton: function (e) {
6401 this.eMouseupButton(e); // don't orphan
6402 this.mouseTarget = e.target;
6403
6404 this.holdTimeout = setTimeout(
6405 $.context(function () {
6406 this.holdInterval = setInterval($.context(function () {
6407 this.stepValue(e.target.value);
6408 }, this), 75);
6409 }, this
6410 ), 500);
6411 },
6412
6413 /**
6414 * Handles re-entering while holding the mouse.
6415 *
6416 * @param e
6417 */
6418 eMouseEnter: function (e) {
6419 if (e.which && e.target == this.mouseTarget) {
6420 this.holdInterval = setInterval($.context(function () {
6421 this.stepValue(e.target.value);
6422 }, this), 75);
6423 }
6424 },
6425
6426 /**
6427 * Handles a mouse-up event on a spinbox button in order to halt rapid repeats.
6428 *
6429 * @param Event e
6430 */
6431 eMouseupButton: function (e) {
6432 clearTimeout(this.holdTimeout);
6433 clearInterval(this.holdInterval);
6434 if (e.type == 'mouseup') {
6435 this.mouseTarget = null;
6436 }
6437 }
6438 };
6439
6440 // *********************************************************************
6441
6442 /**
6443 * Allows an input:checkbox or input:radio to disable subsidiary controls
6444 * based on its own state
6445 *
6446 * @param {Object} $input
6447 */
6448 XenForo.Disabler = function ($input) {
6449 /**
6450 * Sets the disabled state of form elements being controlled by this disabler.
6451 *
6452 * @param Event e
6453 * @param boolean If true, this is the initialization call
6454 */
6455 var setStatus = function (e, init) {
6456 //console.info('Disabler %o for child container: %o', $input, $childContainer);
6457
6458 var $childControls = $childContainer.find('input, select, textarea, button, .inputWrapper, .taggingInput'),
6459 speed = init ? 0 : XenForo.speed.fast,
6460 select = function (e) {
6461 $childContainer.find('input:not([type=hidden], [type=file]), textarea, select, button').first().focus().select();
6462 };
6463
6464 if ($input.is(':checked:enabled')) {
6465 $childContainer
6466 .removeAttr('disabled')
6467 .removeClass('disabled')
6468 .trigger('DisablerDisabled');
6469
6470 $childControls
6471 .removeAttr('disabled')
6472 .removeClass('disabled');
6473
6474 if ($input.hasClass('Hider')) {
6475 if (init) {
6476 $childContainer.show();
6477 } else {
6478 $childContainer.xfFadeDown(speed, init ? null : select);
6479 }
6480 } else if (!init) {
6481 select.call();
6482 }
6483 } else {
6484 if ($input.hasClass('Hider')) {
6485 if (init) {
6486 $childContainer.hide();
6487 } else {
6488 $childContainer.xfFadeUp(speed, null, speed, 'easeInBack');
6489 }
6490 }
6491
6492 $childContainer
6493 .prop('disabled', true)
6494 .addClass('disabled')
6495 .trigger('DisablerEnabled');
6496
6497 $childControls
6498 .prop('disabled', true)
6499 .addClass('disabled')
6500 .each(function (i, ctrl) {
6501 var $ctrl = $(ctrl),
6502 disabledVal = $ctrl.data('disabled');
6503
6504 if (disabledVal !== null && typeof (disabledVal) != 'undefined') {
6505 $ctrl.val(disabledVal);
6506 }
6507 });
6508 }
6509 },
6510
6511 $childContainer = $(
6512 '#' + $input.attr('id') + '_Disabler' + (
6513 $input.data('disabler-target') ? ', ' + $input.data('disabler-target') : ''
6514 )
6515 ),
6516 $form = $input.closest('form');
6517
6518 var setStatusDelayed = function () {
6519 setTimeout(setStatus, 0);
6520 };
6521
6522 if ($input.is(':radio')) {
6523 $form.find('input:radio[name="' + $input.fieldName() + '"]').click(setStatusDelayed);
6524 } else {
6525 $input.click(setStatusDelayed);
6526 }
6527
6528 $form.bind('reset', setStatusDelayed);
6529 $form.bind('XFRecalculate', function () {
6530 setStatus(null, true);
6531 });
6532
6533 setStatus(null, true);
6534
6535 $childContainer.find('label, input, select, textarea').click(function (e) {
6536 if (!$input.is(':checked')) {
6537 $input.prop('checked', true);
6538 setStatus();
6539 }
6540 });
6541
6542 this.setStatus = setStatus;
6543 };
6544
6545 // *********************************************************************
6546
6547 /**
6548 * Quick way to check or toggle all specified items. Works in one of two ways:
6549 * 1) If the control is a checkbox, a data-target attribute specified a jQuery
6550 * selector for a container within which all checkboxes will be toggled
6551 * 2) If the control is something else, the data-target attribute specifies a
6552 * jQuery selector for the elements themselves that will be selected.
6553 *
6554 * @param jQuery .CheckAll
6555 */
6556 XenForo.CheckAll = function ($control) {
6557 if ($control.is(':checkbox')) {
6558 var $target = $control.data('target') ? $($control.data('target')) : false;
6559 if (!$target || !$target.length) {
6560 $target = $control.closest('form');
6561 }
6562
6563 var getCheckBoxes = function () {
6564 var $checkboxes,
6565 filter = $control.data('filter');
6566
6567 $checkboxes = filter
6568 ? $target.find(filter).filter('input:checkbox')
6569 : $target.find('input:checkbox');
6570
6571 return $checkboxes;
6572 };
6573
6574 var setSelectAllState = function () {
6575 var $checkboxes = getCheckBoxes(),
6576 allSelected = $checkboxes.length > 0;
6577
6578 $checkboxes.each(function () {
6579 if ($(this).is($control)) {
6580 return true;
6581 }
6582
6583 if (!$(this).prop('checked')) {
6584 allSelected = false;
6585 return false;
6586 }
6587 });
6588
6589 $control.prop('checked', allSelected);
6590 };
6591 setSelectAllState();
6592
6593 var toggleAllRunning = false;
6594
6595 $target.on('click', 'input:checkbox', function (e) {
6596 if (toggleAllRunning) {
6597 return;
6598 }
6599
6600 var $target = $(e.target);
6601 if ($target.is($control)) {
6602 return;
6603 }
6604
6605 if ($control.data('filter')) {
6606 if (!$target.closest($control.data('filter')).length) {
6607 return;
6608 }
6609 }
6610
6611 setSelectAllState();
6612 });
6613
6614 $control.click(function (e) {
6615 if (toggleAllRunning) {
6616 return;
6617 }
6618
6619 toggleAllRunning = true;
6620 getCheckBoxes().prop('checked', e.target.checked).triggerHandler('click');
6621 toggleAllRunning = false;
6622 });
6623
6624 $control.on('XFRecalculate', setSelectAllState);
6625 } else {
6626 $control.click(function (e) {
6627 var target = $control.data('target');
6628
6629 if (target) {
6630 $(target).prop('checked', true);
6631 }
6632 });
6633 }
6634 };
6635
6636 // *********************************************************************
6637
6638 /**
6639 * Method to allow an input (usually a checkbox) to alter the selection of others.
6640 * When checking the target checkbox, it will also check any controls matching data-check
6641 * and un-check any controls matching data-uncheck
6642 *
6643 * @param jQuery input.AutoChecker[data-check, data-uncheck]
6644 */
6645 XenForo.AutoChecker = function ($control) {
6646 $control.click(function (e) {
6647 if (this.checked) {
6648 var selector = null;
6649
6650 $.each({ check: true, uncheck: false }, function (dataField, checkState) {
6651 if (selector = $control.data(dataField)) {
6652 $(selector).each(function () {
6653 this.checked = checkState;
6654
6655 var Disabler = $(this).data('XenForo.Disabler');
6656
6657 if (typeof Disabler == 'object') {
6658 Disabler.setStatus();
6659 }
6660 });
6661 }
6662 });
6663 }
6664 });
6665 };
6666
6667 // *********************************************************************
6668
6669 /**
6670 * Converts a checkbox/radio plus label into a toggle button.
6671 *
6672 * @param jQuery label.ToggleButton
6673 */
6674 XenForo.ToggleButton = function ($label) {
6675 var $button,
6676
6677 setCheckedClasses = function () {
6678 $button[($input.is(':checked') ? 'addClass' : 'removeClass')]('checked');
6679 },
6680
6681 $input = $label.hide().find('input:checkbox, input:radio').first(),
6682
6683 $list = $label.closest('ul, ol').bind('toggleButtonClick', setCheckedClasses);
6684
6685 if (!$input.length && $label.attr('for')) {
6686 $input = $('#' + $label.attr('for'));
6687 }
6688
6689 $button = $('<a />')
6690 .text($label.attr('title') || $label.text())
6691 .insertBefore($label)
6692 .attr(
6693 {
6694 'class': 'button ' + $label.attr('class'),
6695 'title': $label.text()
6696 })
6697 .click(function (e) {
6698 $input.click();
6699
6700 if ($list.length) {
6701 $list.triggerHandler('toggleButtonClick');
6702 } else {
6703 setCheckedClasses();
6704 }
6705
6706 return false;
6707 });
6708
6709 $label.closest('form').bind('reset', function (e) {
6710 setTimeout(setCheckedClasses, 100);
6711 });
6712
6713 setCheckedClasses();
6714 };
6715
6716 // *********************************************************************
6717
6718 /**
6719 * Allows files to be uploaded in-place without a page refresh
6720 *
6721 * @param jQuery form.AutoInlineUploader
6722 */
6723 /**
6724 * Allows files to be uploaded in-place without a page refresh
6725 *
6726 * @param jQuery form.AutoInlineUploader
6727 */
6728 XenForo.AutoInlineUploader = function ($form) {
6729 /**
6730 * Fires when the contents of an input:file change.
6731 * Submits the form into a temporary iframe.
6732 *
6733 * @param event e
6734 */
6735 var $uploader = $form.find('input:file').each(function () {
6736 var $target = $(this).change(function (e) {
6737 if ($(e.target).val() != '') {
6738 var $iframe,
6739 $hiddenInput;
6740 $('.bootstrap-filestyle').find('label').changeElementType('fake_label');
6741 $iframe = $('<iframe src="about:blank" style="display:none; background-color: white" name="AutoInlineUploader"></iframe>')
6742 .insertAfter($(e.target))
6743 .load(function (e) {
6744 $('.bootstrap-filestyle').find('fake_label').changeElementType('label');
6745 var $iframe = $(e.target);
6746 $iframe.contents().find('style').remove();
6747 var ajaxData = $iframe.contents().text(),
6748 eComplete = null;
6749
6750 // Opera fires this function when it's not done with no data
6751 if (!ajaxData) {
6752 return false;
6753 }
6754
6755 // alert the global progress indicator that the transfer is complete
6756 $(document).trigger('PseudoAjaxStop');
6757
6758 $uploader = $uploaderOrig.clone(true).replaceAll($target);
6759
6760 // removing the iframe after a delay to prevent Firefox' progress indicator staying active
6761 setTimeout(function () {
6762 $iframe.remove();
6763 }, 500);
6764
6765 try {
6766 ajaxData = $.parseJSON(ajaxData);
6767 console.info('Inline file upload completed successfully. Data: %o', ajaxData);
6768 } catch (e) {
6769 console.error(ajaxData);
6770 return false;
6771 }
6772
6773 if (XenForo.hasResponseError(ajaxData)) {
6774 return false;
6775 }
6776
6777 $('input:submit', this.$form).removeAttr('disabled');
6778
6779 eComplete = new $.Event('AutoInlineUploadComplete');
6780 eComplete.$form = $form;
6781 eComplete.ajaxData = ajaxData;
6782
6783 $form.trigger(eComplete);
6784
6785 $('#ctrl_avatar').attr('style', 'position: absolute; clip: rect(0px, 0px, 0px, 0px);');
6786 $form.xfActivate();
6787 if (!eComplete.isDefaultPrevented() && (ajaxData.message || ajaxData._redirectMessage)) {
6788 XenForo.alert(ajaxData.message || ajaxData._redirectMessage, '', 2500);
6789 }
6790 });
6791
6792 $hiddenInput = $('<span>'
6793 + '<input type="hidden" name="_xfNoRedirect" value="1" />'
6794 + '<input type="hidden" name="_xfResponseType" value="json-text" />'
6795 + '<input type="hidden" name="_xfUploader" value="1" />'
6796 + '</span>')
6797 .appendTo($form);
6798
6799 $form.attr('target', 'AutoInlineUploader')
6800 .submit()
6801 .trigger('AutoInlineUploadStart');
6802
6803 $hiddenInput.remove();
6804
6805 // fire the event that will be caught by the global progress indicator
6806 $(document).trigger('PseudoAjaxStart');
6807
6808 $form.find('input:submit').prop('disabled', true);
6809 }
6810 }),
6811
6812 $uploaderOrig = $target.clone(true);
6813 });
6814 };
6815
6816 // *********************************************************************
6817
6818 XenForo.MultiSubmitFix = function ($form) {
6819 const selector = 'input:submit, input:reset, input.PreviewButton, input.DisableOnSubmit',
6820 enable = function () {
6821 $(window).unbind('pagehide', enable);
6822 $(document).unbind('ajaxStop', enable)
6823
6824 $form.trigger('EnableSubmitButtons').find(selector)
6825 .removeClass('disabled')
6826 .removeAttr('disabled');
6827 },
6828 disable = function () {
6829 setTimeout(function () {
6830 /**
6831 * Workaround for a Firefox issue that prevents resubmission after back button,
6832 * however the workaround triggers a webkit rendering bug.
6833 */
6834 if (!$.browser.webkit) {
6835 $(window).bind('pagehide', enable);
6836 }
6837
6838 $form.trigger('DisableSubmitButtons').find(selector)
6839 .prop('disabled', true)
6840 .addClass('disabled');
6841 }, 0);
6842
6843 $(document).one('ajaxStop', function () {
6844 setTimeout(enable, 3000);
6845 });
6846 };
6847
6848 $form.data('MultiSubmitEnable', enable)
6849 .data('MultiSubmitDisable', disable)
6850 .submit(disable);
6851
6852 return enable;
6853 };
6854
6855 // *********************************************************************
6856
6857 /**
6858 * Handler for radio/checkbox controls that cause the form to submit when they are altered
6859 *
6860 * @param jQuery input:radio.SubmitOnChange, input:checkbox.SubmitOnChange, label.SubmitOnChange
6861 */
6862 XenForo.SubmitOnChange = function ($input) {
6863 if ($input.is('label')) {
6864 $input = $input.find('input:radio, input:checkbox');
6865 if (!$input.length) {
6866 return;
6867 }
6868 }
6869
6870 $input.click(function (e) {
6871 clearTimeout(e.target.form.submitTimeout);
6872
6873 e.target.form.submitTimeout = setTimeout(function () {
6874 $(e.target).closest('form').submit();
6875 }, 500);
6876 });
6877 };
6878
6879 // *********************************************************************
6880
6881 /**
6882 * Handler for automatic AJAX form validation and error management
6883 *
6884 * Forms to be auto-validated require the following attributes:
6885 *
6886 * * data-fieldValidatorUrl: URL of a JSON-returning validator for a single field, using _POST keys of 'name' and 'value'
6887 * * data-optInOut: (Optional - default = OptOut) Either OptIn or OptOut, depending on the validation mode. Fields with a class of OptIn are included in opt-in mode, while those with OptOut are excluded in opt-out mode.
6888 * * data-exitUrl: (Optional - no default) If defined, any form reset event will redirect to this URL.
6889 * * data-existingDataKey: (Optional) Specifies the primary key of the data being manipulated. If this is not present, a hidden input with class="ExistingDataKey" is searched for.
6890 * * data-redirect: (Optional) If set, the browser will redirect to the returned _redirectTarget from the ajaxData response after validation
6891 *
6892 * @param jQuery form.AutoValidator
6893 */
6894 XenForo.AutoValidator = function ($form) {
6895 this.__construct($form);
6896 };
6897 XenForo.AutoValidator.prototype =
6898 {
6899 __construct: function ($form) {
6900 this.$form = $form.bind(
6901 {
6902 submit: $.context(this, 'ajaxSave'),
6903 reset: $.context(this, 'formReset'),
6904 BbCodeWysiwygEditorAutoSave: $.context(this, 'editorAutoSave')
6905 });
6906
6907 this.$form.find('input[type="submit"]').click($.context(this, 'setClickedSubmit'));
6908
6909 this.fieldValidatorUrl = this.$form.data('fieldvalidatorurl');
6910 this.optInMode = this.$form.data('optinout') || 'OptOut';
6911 this.ajaxSubmit = (XenForo.isPositive(this.$form.data('normalsubmit')) ? false : true);
6912 this.submitPending = false;
6913
6914 this.fieldValidationTimeouts = {};
6915 this.fieldValidationRequests = {};
6916 },
6917
6918 /**
6919 * Fetches the value of the form's existing data key.
6920 *
6921 * This could either be a data-existingDataKey attribute on the form itself,
6922 * or a hidden input with class 'ExistingDataKey'
6923 *
6924 * @return string
6925 */
6926 getExistingDataKey: function () {
6927 var val = this.$form.find('input.ExistingDataKey, select.ExistingDataKey, textarea.ExistingDataKey, button.ExistingDataKey').val();
6928 if (val === undefined) {
6929 val = this.$form.data('existingdatakey');
6930 if (val === undefined) {
6931 val = '';
6932 }
6933 }
6934
6935 return val;
6936 },
6937
6938 /**
6939 * Intercepts form reset events.
6940 * If the form specifies a data-exitUrl, the browser will navigate there before resetting the form.
6941 *
6942 * @param event e
6943 */
6944 formReset: function (e) {
6945 var exitUrl = this.$form.data('exiturl');
6946
6947 if (exitUrl) {
6948 XenForo.redirect(exitUrl);
6949 }
6950 },
6951
6952 /**
6953 * Fires whenever a submit button is clicked, in order to store the clicked control
6954 *
6955 * @param event e
6956 */
6957 setClickedSubmit: function (e) {
6958 this.$form.data('clickedsubmitbutton', e.target);
6959 },
6960
6961 editorAutoSave: function (e) {
6962 if (this.submitPending) {
6963 e.preventDefault();
6964 }
6965 },
6966
6967 /**
6968 * Intercepts form submit events.
6969 * Attempts to save the form with AJAX, after cancelling any pending validation tasks.
6970 *
6971 * @param event e
6972 *
6973 * @return boolean false
6974 */
6975 ajaxSave: function (e) {
6976 if (!this.ajaxSubmit || !XenForo._enableAjaxSubmit) {
6977 // do normal validation
6978 return true;
6979 }
6980
6981 this.abortPendingFieldValidation();
6982
6983 var clickedSubmitButton = this.$form.data('clickedsubmitbutton'),
6984 serialized,
6985 $clickedSubmitButton,
6986
6987 /**
6988 * Event listeners for this event can:
6989 * e.preventSubmit = true; to prevent any submission
6990 * e.preventDefault(); to disable ajax sending
6991 */
6992 eDataSend = $.Event('AutoValidationBeforeSubmit');
6993 eDataSend.formAction = this.$form.attr('action');
6994 eDataSend.clickedSubmitButton = clickedSubmitButton;
6995 eDataSend.preventSubmit = false;
6996 eDataSend.ajaxOptions = {};
6997
6998 this.$form.trigger(eDataSend);
6999
7000 this.$form.removeData('clickedSubmitButton');
7001
7002 if (eDataSend.preventSubmit) {
7003 return false;
7004 } else if (!eDataSend.isDefaultPrevented()) {
7005 serialized = this.$form.serializeArray();
7006 if (clickedSubmitButton) {
7007 $clickedSubmitButton = $(clickedSubmitButton);
7008 if ($clickedSubmitButton.attr('name')) {
7009 serialized.push({
7010 name: $clickedSubmitButton.attr('name'),
7011 value: $clickedSubmitButton.attr('value')
7012 });
7013 }
7014 }
7015
7016 this.submitPending = true;
7017
7018 XenForo.ajax(
7019 eDataSend.formAction,
7020 serialized,
7021 $.context(this, 'ajaxSaveResponse'),
7022 eDataSend.ajaxOptions
7023 );
7024
7025 e.preventDefault();
7026 }
7027 },
7028
7029 /**
7030 * Handles the AJAX response from ajaxSave().
7031 *
7032 * @param ajaxData
7033 * @param textStatus
7034 * @return
7035 */
7036 ajaxSaveResponse: function (ajaxData, textStatus) {
7037 this.submitPending = false;
7038
7039 if (!ajaxData) {
7040 console.warn('No ajax data returned.');
7041 return false;
7042 }
7043
7044 var eDataRecv,
7045 eError,
7046 eComplete,
7047 $trigger;
7048
7049 eDataRecv = $.Event('AutoValidationDataReceived');
7050 eDataRecv.ajaxData = ajaxData;
7051 eDataRecv.textStatus = textStatus;
7052 eDataRecv.validationError = [];
7053 console.group('Event: %s', eDataRecv.type);
7054 this.$form.trigger(eDataRecv);
7055 console.groupEnd();
7056 if (eDataRecv.isDefaultPrevented()) {
7057 return false;
7058 }
7059
7060 // if the submission has failed validation, show the error overlay
7061 if (!this.validates(eDataRecv)) {
7062 eError = $.Event('AutoValidationError');
7063 eError.ajaxData = ajaxData;
7064 eError.textStatus = textStatus;
7065 eError.validationError = eDataRecv.validationError;
7066 console.group('Event: %s', eError.type);
7067 this.$form.trigger(eError);
7068 console.groupEnd();
7069 if (eError.isDefaultPrevented()) {
7070 return false;
7071 }
7072
7073 if (this.$form.closest('.xenOverlay').length) {
7074 this.$form.closest('.xenOverlay').data('overlay').close();
7075 }
7076
7077 if (ajaxData.errorTemplateHtml) {
7078 new XenForo.ExtLoader(ajaxData, function (data) {
7079 var $overlayHtml = XenForo.alert(
7080 ajaxData.errorTemplateHtml,
7081 XenForo.phrases.following_error_occurred + ':'
7082 );
7083 if ($overlayHtml) {
7084 $overlayHtml.find('div.errorDetails').removeClass('baseHtml');
7085 if (ajaxData.errorOverlayType) {
7086 $overlayHtml.closest('.errorOverlay').removeClass('errorOverlay').addClass(ajaxData.errorOverlayType);
7087 }
7088 }
7089 });
7090 } else if (ajaxData.templateHtml) {
7091 setTimeout($.context(function () {
7092 this.$error = XenForo.createOverlay(null, this.prepareError(ajaxData.templateHtml)).load();
7093 }, this), 250);
7094 } else if (ajaxData.error !== undefined) {
7095 if (typeof ajaxData.error === 'object') {
7096 var key;
7097 for (key in ajaxData.error) {
7098 break;
7099 }
7100 ajaxData.error = ajaxData.error[key];
7101 }
7102
7103 XenForo.alert(
7104 ajaxData.error + '\n'
7105 + (ajaxData.traceHtml !== undefined ? '<ol class="traceHtml">\n' + ajaxData.traceHtml + '</ol>' : ''),
7106 XenForo.phrases.following_error_occurred + ':'
7107 );
7108 }
7109
7110 return false;
7111 }
7112
7113 eComplete = $.Event('AutoValidationComplete'),
7114 eComplete.ajaxData = ajaxData;
7115 eComplete.textStatus = textStatus;
7116 eComplete.$form = this.$form;
7117 console.group('Event: %s', eComplete.type);
7118 this.$form.trigger(eComplete);
7119 console.groupEnd();
7120 if (eComplete.isDefaultPrevented()) {
7121 return false;
7122 }
7123
7124 // if the form is in an overlay, close it
7125 if (this.$form.parents('.xenOverlay').length) {
7126 var $overlay = this.$form.parents('.xenOverlay').data('overlay');
7127
7128 if (ajaxData.linkPhrase) {
7129 $trigger = $overlay.getTrigger();
7130 $trigger.xfFadeOut(XenForo.speed.fast, function () {
7131 if (ajaxData.linkUrl && $trigger.is('a')) {
7132 $trigger.attr('href', ajaxData.linkUrl);
7133 }
7134
7135 $trigger
7136 .text(ajaxData.linkPhrase)
7137 .xfFadeIn(XenForo.speed.fast);
7138 });
7139 }
7140
7141 // if animations are disabled, decaching can happen too quickly
7142 setTimeout(function () {
7143 $overlay.close();
7144 }, 0);
7145 }
7146
7147 if (XenForo.isPositive(this.$form.data('reset'))) {
7148 this.$form[0].reset();
7149 }
7150
7151 if (ajaxData.message) {
7152 XenForo.alert(ajaxData.message, '', 4000);
7153 return;
7154 }
7155
7156 // if a redirect message was not specified, redirect immediately
7157 if (ajaxData._redirectMessage == '') {
7158 this.submitPending = true;
7159 return this.redirect(ajaxData._redirectTarget);
7160 }
7161
7162 // show the redirect message, then redirect if a redirect target was specified
7163 this.submitPending = true;
7164 XenForo.alert(ajaxData._redirectMessage, '', 1000, $.context(function () {
7165 this.redirect(ajaxData._redirectTarget);
7166 }, this));
7167 },
7168
7169 /**
7170 * Checks for the presence of validation errors in the given event
7171 *
7172 * @param event e
7173 *
7174 * @return boolean
7175 */
7176 validates: function (e) {
7177 return ($.isEmptyObject(e.validationErrors) && !e.ajaxData.error);
7178 },
7179
7180 /**
7181 * Attempts to match labels to errors for the error overlay
7182 *
7183 * @param string html
7184 *
7185 * @return jQuery
7186 */
7187 prepareError: function (html) {
7188 $html = $(html);
7189
7190 // extract labels that correspond to the error fields and insert their text next to the error message
7191 $html.find('label').each(function (i, label) {
7192 var $ctrlLabel = $('#' + $(label).attr('for'))
7193 .closest('.ctrlUnit')
7194 .find('dt > label');
7195
7196 if ($ctrlLabel.length) {
7197 $(label).prepend($ctrlLabel.text() + '<br />');
7198 }
7199 });
7200
7201 return $html;
7202 },
7203
7204 /**
7205 * Redirect the browser to redirectTarget if it is specified
7206 *
7207 * @param string redirectTarget
7208 *
7209 * @return boolean
7210 */
7211 redirect: function (redirectTarget) {
7212 if (XenForo.isPositive(this.$form.data('redirect')) || !parseInt(XenForo._enableOverlays)) {
7213 var $AutoValidationRedirect = new $.Event('AutoValidationRedirect');
7214 $AutoValidationRedirect.redirectTarget = redirectTarget;
7215
7216 this.$form.trigger($AutoValidationRedirect);
7217
7218 if (!$AutoValidationRedirect.isDefaultPrevented() && $AutoValidationRedirect.redirectTarget) {
7219 var fn = function () {
7220 XenForo.redirect(redirectTarget);
7221 };
7222
7223 if (XenForo._manualDeferOverlay) {
7224 $(document).one('ManualDeferComplete', fn);
7225 } else {
7226 fn();
7227 }
7228
7229 return true;
7230 }
7231 }
7232
7233 return false;
7234 },
7235
7236 // ---------------------------------------------------
7237 // Field validation methods...
7238
7239 /**
7240 * Sets a timeout before an AJAX field validation request will be fired
7241 * (Prevents AJAX floods)
7242 *
7243 * @param string Name of field to be validated
7244 * @param function Callback to fire when the timeout elapses
7245 */
7246 setFieldValidationTimeout: function (name, callback) {
7247 if (!this.hasFieldValidator(name)) {
7248 return false;
7249 }
7250
7251 console.log('setTimeout %s', name);
7252
7253 this.clearFieldValidationTimeout(name);
7254
7255 this.fieldValidationTimeouts[name] = setTimeout(callback, 250);
7256 },
7257
7258 /**
7259 * Cancels a timeout set with setFieldValidationTimeout()
7260 *
7261 * @param string name
7262 */
7263 clearFieldValidationTimeout: function (name) {
7264 if (this.fieldValidationTimeouts[name]) {
7265 console.log('Clear field validation timeout: %s', name);
7266
7267 clearTimeout(this.fieldValidationTimeouts[name]);
7268 delete (this.fieldValidationTimeouts[name]);
7269 }
7270 },
7271
7272 /**
7273 * Fires an AJAX field validation request
7274 *
7275 * @param string Name of variable to be verified
7276 * @param jQuery Input field to be validated
7277 * @param function Callback function to fire on success
7278 */
7279 startFieldValidationRequest: function (name, $input, callback) {
7280 if (!this.hasFieldValidator(name)) {
7281 return false;
7282 }
7283
7284 // abort any existing AJAX validation requests from this $input
7285 this.abortFieldValidationRequest(name);
7286
7287 // fire the AJAX request and register it in the fieldValidationRequests
7288 // object so it can be cancelled by subsequent requests
7289 this.fieldValidationRequests[name] = XenForo.ajax(this.fieldValidatorUrl,
7290 {
7291 name: name,
7292 value: $input.fieldValue(),
7293 existingDataKey: this.getExistingDataKey()
7294 }, callback,
7295 {
7296 global: false // don't show AJAX progress indicators for inline validation
7297 });
7298 },
7299
7300 /**
7301 * Aborts an AJAX field validation request set up by startFieldValidationRequest()
7302 *
7303 * @param string name
7304 */
7305 abortFieldValidationRequest: function (name) {
7306 if (this.fieldValidationRequests[name]) {
7307 console.log('Abort field validation request: %s', name);
7308
7309 this.fieldValidationRequests[name].abort();
7310 delete (this.fieldValidationRequests[name]);
7311 }
7312 },
7313
7314 /**
7315 * Cancels any pending timeouts or ajax field validation requests
7316 */
7317 abortPendingFieldValidation: function () {
7318 $.each(this.fieldValidationTimeouts, $.context(this, 'clearFieldValidationTimeout'));
7319 $.each(this.fieldValidationRequests, $.context(this, 'abortFieldValidationRequest'));
7320 },
7321
7322 /**
7323 * Throws a warning if this.fieldValidatorUrl is not valid
7324 *
7325 * @param string Name of field to be validated
7326 *
7327 * @return boolean
7328 */
7329 hasFieldValidator: function (name) {
7330 if (this.fieldValidatorUrl) {
7331 return true;
7332 }
7333
7334 //console.warn('Unable to request validation for field "%s" due to lack of fieldValidatorUrl in form tag.', name);
7335 return false;
7336 }
7337 };
7338
7339 // *********************************************************************
7340
7341 /**
7342 * Handler for individual fields in an AutoValidator form.
7343 * Manages individual field validation and inline error display.
7344 *
7345 * @param jQuery input [text-type]
7346 */
7347 XenForo.AutoValidatorControl = function ($input) {
7348 this.__construct($input);
7349 };
7350 XenForo.AutoValidatorControl.prototype =
7351 {
7352 __construct: function ($input) {
7353 this.$form = $input.closest('form.AutoValidator').bind(
7354 {
7355 AutoValidationDataReceived: $.context(this, 'handleFormValidation')
7356 });
7357
7358 this.$input = $input.bind(
7359 {
7360 change: $.context(this, 'change'),
7361 AutoValidationError: $.context(this, 'showError'),
7362 AutoValidationPass: $.context(this, 'hideError')
7363 });
7364
7365 this.name = $input.data('validatorname') || $input.attr('name');
7366 this.autoValidate = $input.hasClass('NoAutoValidate') ? false : true;
7367 },
7368
7369 /**
7370 * When the value of a field changes, initiate validation
7371 *
7372 * @param event e
7373 */
7374 change: function (e) {
7375 if (this.autoValidate) {
7376 this.$form.data('XenForo.AutoValidator')
7377 .setFieldValidationTimeout(this.name, $.context(this, 'validate'));
7378 }
7379 },
7380
7381 /**
7382 * Fire a validation AJAX request
7383 */
7384 validate: function () {
7385 if (this.autoValidate) {
7386 this.$form.data('XenForo.AutoValidator')
7387 .startFieldValidationRequest(this.name, this.$input, $.context(this, 'handleValidation'));
7388 }
7389 },
7390
7391 /**
7392 * Handle the data returned from an AJAX validation request fired in validate().
7393 * Fires 'AutoValidationPass' or 'AutoValidationError' for the $input according to the validation state.
7394 *
7395 * @param object ajaxData
7396 * @param string textStatus
7397 *
7398 * @return boolean
7399 */
7400 handleValidation: function (ajaxData, textStatus) {
7401 if (ajaxData && ajaxData.error && ajaxData.error.hasOwnProperty(this.name)) {
7402 this.$input.trigger({
7403 type: 'AutoValidationError',
7404 errorMessage: ajaxData.error[this.name]
7405 });
7406 return false;
7407 } else {
7408 this.$input.trigger('AutoValidationPass');
7409 return true;
7410 }
7411 },
7412
7413 /**
7414 * Shows an inline error message, text contained within a .errorMessage property of the event passed
7415 *
7416 * @param event e
7417 */
7418 showError: function (e) {
7419 console.warn('%s: %s', this.name, e.errorMessage);
7420
7421 var error = this.fetchError(e.errorMessage).css('display', 'inline-block');
7422 this.positionError(error);
7423 },
7424
7425 /**
7426 * Hides any inline error message shown with this input
7427 */
7428 hideError: function () {
7429 console.info('%s: Okay', this.name);
7430
7431 if (this.$error) {
7432 this.fetchError()
7433 .hide();
7434 }
7435 },
7436
7437 /**
7438 * Fetches or creates (as necessary) the error HTML object for this field
7439 *
7440 * @param string Error message
7441 *
7442 * @return jQuery this.$error
7443 */
7444 fetchError: function (message) {
7445 if (!this.$error) {
7446 this.$error = $('<label for="' + this.$input.attr('id') + '" class="formValidationInlineError">WHoops</label>').insertAfter(this.$input);
7447 }
7448
7449 if (message) {
7450 this.$error.html(message).xfActivate();
7451 }
7452
7453 return this.$error;
7454 },
7455
7456 /**
7457 * Returns an object containing top and left properties, used to position the inline error message
7458 */
7459 positionError: function ($error) {
7460 $error.removeClass('inlineError');
7461
7462 var coords = this.$input.coords('outer', 'position'),
7463 screenCoords = this.$input.coords('outer'),
7464 $window = $(window),
7465 outerWidth = $error.outerWidth(),
7466 absolute,
7467 position = { top: coords.top };
7468
7469 if (!screenCoords.width || !screenCoords.height) {
7470 absolute = false;
7471 } else {
7472 if (XenForo.isRTL()) {
7473 position.left = coords.left - outerWidth - 10;
7474 absolute = (screenCoords.left - outerWidth - 10 > 0);
7475 } else {
7476 var screenLeft = screenCoords.left + screenCoords.width + 10;
7477
7478 absolute = screenLeft + outerWidth < ($window.width() + $window.scrollLeft());
7479 position.left = coords.left + coords.width + 10;
7480 }
7481 }
7482
7483 if (absolute) {
7484 $error.css(position);
7485 } else {
7486 $error.addClass('inlineError');
7487 }
7488 },
7489
7490 /**
7491 * Handles validation for this field passed down from a submission of the whole AutoValidator
7492 * form, and passes the relevant data into the handler for this field specifically.
7493 *
7494 * @param event e
7495 */
7496 handleFormValidation: function (e) {
7497 if (!this.handleValidation(e.ajaxData, e.textStatus)) {
7498 e.validationError.push(this.name);
7499 }
7500 }
7501 };
7502
7503 // *********************************************************************
7504
7505 /**
7506 * Checks a form field to see if it is part of an AutoValidator form,
7507 * and if so, whether or not it is subject to autovalidation.
7508 *
7509 * @param object Form control to be tested
7510 *
7511 * @return boolean
7512 */
7513 XenForo.isAutoValidatorField = function (ctrl) {
7514 var AutoValidator, $ctrl, $form = $(ctrl.form);
7515
7516 if (!$form.hasClass('AutoValidator')) {
7517 return false;
7518 }
7519
7520 AutoValidator = $form.data('XenForo.AutoValidator');
7521
7522 if (AutoValidator) {
7523 $ctrl = $(ctrl);
7524
7525 switch (AutoValidator.optInMode) {
7526 case 'OptIn': {
7527 return ($ctrl.hasClass('OptIn') || $ctrl.closest('.ctrlUnit').hasClass('OptIn'));
7528 }
7529 default: {
7530 return (!$ctrl.hasClass('OptOut') && !$ctrl.closest('.ctrlUnit').hasClass('OptOut'));
7531 }
7532 }
7533 }
7534
7535 return false;
7536 };
7537
7538 // *********************************************************************
7539
7540 XenForo.PreviewForm = function ($form) {
7541 var previewUrl = $form.data('previewurl');
7542 if (!previewUrl) {
7543 console.warn('PreviewForm has no data-previewUrl: %o', $form);
7544 return;
7545 }
7546
7547 $form.find('.PreviewButton').click(function (e) {
7548 var $button = $(this);
7549
7550 XenForo.ajax(previewUrl, $form.serialize(), function (ajaxData) {
7551 if (XenForo.hasResponseError(ajaxData) || !XenForo.hasTemplateHtml(ajaxData)) {
7552 return false;
7553 }
7554
7555 new XenForo.ExtLoader(ajaxData, function (ajaxData) {
7556 var $preview = $form.find('.PreviewContainer').first();
7557 if ($preview.length) {
7558 $preview.xfFadeOut(XenForo.speed.fast, function () {
7559 $preview.html(ajaxData.templateHtml).xfActivate();
7560 });
7561 } else {
7562 $preview = $('<div />', { 'class': 'PreviewContainer' })
7563 .hide()
7564 .html(ajaxData.templateHtml)
7565 .prependTo($form)
7566 .xfActivate();
7567 }
7568
7569 var overlay = $button.data('overlay');
7570 if (overlay) {
7571 $preview.show();
7572 XenForo.createOverlay($preview, $preview.html(ajaxData.templateHtml)).load();
7573 } else {
7574 $preview.xfFadeIn(XenForo.speed.fast);
7575 $preview.get(0).scrollIntoView(true);
7576 }
7577 });
7578 });
7579 });
7580 };
7581
7582 // *********************************************************************
7583
7584 /**
7585 * Allows a text input field to rewrite the H1 (or equivalent) tag's contents
7586 *
7587 * @param jQuery input[data-liveTitleTemplate]
7588 */
7589 XenForo.LiveTitle = function ($input) {
7590 var $title = $input.closest('.formOverlay').find('h2.h1'), setTitle;
7591
7592 if (!$title.length) {
7593 $title = $('.titleBar h1').first();
7594 }
7595 console.info('Title Element: %o', $title);
7596 $title.data('originalhtml', $title.html());
7597
7598 setTitle = function (value) {
7599 $input.trigger('LiveTitleSet', [value]);
7600
7601 $title.html(value === ''
7602 ? $title.data('originalhtml')
7603 : $input.data('livetitletemplate').replace(/%s/, $('<div />').text(value).html())
7604 );
7605 };
7606
7607 if (!$input.hasClass('prompt')) {
7608 setTitle($input.strval());
7609 }
7610
7611 $('.modal').on('hidden.bs.modal', function () {
7612 setTitle('')
7613 })
7614
7615 $input.bind('keyup focus', function (e) {
7616 setTitle($input.strval());
7617 })
7618 .on('paste', function (e) {
7619 setTimeout(function () {
7620 setTitle($input.strval());
7621 }, 0);
7622 })
7623 .closest('form').bind('reset', function (e) {
7624 setTitle('');
7625 });
7626 };
7627
7628 // *********************************************************************
7629
7630 XenForo.TextareaElastic = function ($input) {
7631 this.__construct($input);
7632 };
7633 XenForo.TextareaElastic.prototype =
7634 {
7635 __construct: function ($input) {
7636 this.$input = $input;
7637 this.curHeight = 0;
7638
7639 $input.bind('keyup focus XFRecalculate', $.context(this, 'recalculate'));
7640 $input.bind('paste', $.context(this, 'paste'));
7641
7642 if ($input.val() !== '') {
7643 this.recalculate();
7644 }
7645 },
7646
7647 recalculate: function () {
7648 var $input = this.$input,
7649 input = $input.get(0),
7650 clone,
7651 height,
7652 pos;
7653
7654 if ($input.val() === '') {
7655 $input.css({
7656 'overflow-y': 'hidden',
7657 'height': ''
7658 });
7659 this.curHeight = 0;
7660 return;
7661 }
7662
7663 if (!input.clientWidth) {
7664 return;
7665 }
7666
7667 if (!this.minHeight) {
7668 this.borderBox = ($input.css('-moz-box-sizing') == 'border-box' || $input.css('box-sizing') == 'border-box');
7669 this.minHeight = (this.borderBox ? $input.outerHeight() : input.clientHeight);
7670
7671 if (!this.minHeight) {
7672 return;
7673 }
7674
7675 this.maxHeight = parseInt($input.css('max-height'), 10);
7676 this.spacing = (this.borderBox ? $input.outerHeight() - $input.innerHeight() : 0);
7677 }
7678
7679 if (!this.$clone) {
7680 this.$clone = $('<textarea />').css({
7681 position: 'absolute',
7682 left: (XenForo.isRTL() ? '9999em' : '-9999em'),
7683 top: 0,
7684 visibility: 'hidden',
7685 width: input.clientWidth,
7686 height: '1px',
7687 'font-size': $input.css('font-size'),
7688 'font-family': $input.css('font-family'),
7689 'font-weight': $input.css('font-weight'),
7690 'line-height': $input.css('line-height'),
7691 'word-wrap': $input.css('word-wrap')
7692 }).attr('tabindex', -1).val(' ');
7693
7694 this.$clone.appendTo(document.body);
7695
7696 this.lineHeight = this.$clone.get(0).scrollHeight;
7697 }
7698
7699 this.$clone.val($input.val());
7700 clone = this.$clone.get(0);
7701
7702 height = Math.max(this.minHeight, clone.scrollHeight + this.lineHeight + this.spacing);
7703
7704 if (height < this.maxHeight) {
7705 if (this.curHeight != height) {
7706 input = $input.get(0);
7707 if (this.curHeight == this.maxHeight && input.setSelectionRange) {
7708 pos = input.selectionStart;
7709 }
7710
7711 $input.css({
7712 'overflow-y': 'hidden',
7713 'height': height + 'px'
7714 });
7715
7716 if (this.curHeight == this.maxHeight && input.setSelectionRange) {
7717 try {
7718 input.setSelectionRange(pos, pos);
7719 } catch (e) {
7720 }
7721 }
7722
7723 this.curHeight = height;
7724 }
7725 } else {
7726 if (this.curHeight != this.maxHeight) {
7727 input = $input.get(0);
7728 if (input.setSelectionRange) {
7729 pos = input.selectionStart;
7730 }
7731
7732 $input.css({
7733 'overflow-y': 'auto',
7734 'height': this.maxHeight + 'px'
7735 });
7736
7737 if (input.setSelectionRange) {
7738 try {
7739 input.setSelectionRange(pos, pos);
7740 } catch (e) {
7741 }
7742 }
7743
7744 this.curHeight = this.maxHeight;
7745 }
7746 }
7747 },
7748
7749 paste: function () {
7750 setTimeout($.context(this, 'recalculate'), 100);
7751 }
7752 };
7753
7754 // *********************************************************************
7755
7756 XenForo.AutoTimeZone = function ($element) {
7757 var now = new Date(),
7758 jan1 = new Date(now.getFullYear(), 0, 1), // 0 = jan
7759 jun1 = new Date(now.getFullYear(), 5, 1), // 5 = june
7760 jan1offset = Math.round(jan1.getTimezoneOffset()),
7761 jun1offset = Math.round(jun1.getTimezoneOffset());
7762
7763 // opera doesn't report TZ offset differences in jan/jun correctly
7764 if ($.browser.opera) {
7765 return false;
7766 }
7767
7768 if (XenForo.AutoTimeZone.map[jan1offset + ',' + jun1offset]) {
7769 $element.val(XenForo.AutoTimeZone.map[jan1offset + ',' + jun1offset]);
7770 return true;
7771 } else {
7772 return false;
7773 }
7774 };
7775
7776 XenForo.AutoTimeZone.map =
7777 {
7778 '660,660': 'Pacific/Midway',
7779 '600,600': 'Pacific/Honolulu',
7780 '570,570': 'Pacific/Marquesas',
7781 '540,480': 'America/Anchorage',
7782 '480,420': 'America/Los_Angeles',
7783 '420,360': 'America/Denver',
7784 '420,420': 'America/Phoenix',
7785 '360,300': 'America/Chicago',
7786 '360,360': 'America/Belize',
7787 '300,240': 'America/New_York',
7788 '300,300': 'America/Bogota',
7789 '270,270': 'America/Caracas',
7790 '240,180': 'America/Halifax',
7791 '180,240': 'America/Cuiaba',
7792 '240,240': 'America/La_Paz',
7793 '210,150': 'America/St_Johns',
7794 '180,180': 'America/Argentina/Buenos_Aires',
7795 '120,180': 'America/Sao_Paulo',
7796 '180,120': 'America/Miquelon',
7797 '120,120': 'America/Noronha',
7798 '60,60': 'Atlantic/Cape_Verde',
7799 '60,0': 'Atlantic/Azores',
7800 '0,-60': 'Europe/London',
7801 '0,0': 'Atlantic/Reykjavik',
7802 '-60,-120': 'Europe/Amsterdam',
7803 '-60,-60': 'Africa/Algiers',
7804 '-120,-60': 'Africa/Windhoek',
7805 '-120,-180': 'Europe/Athens',
7806 '-120,-120': 'Africa/Johannesburg',
7807 '-180,-240': 'Africa/Nairobi',
7808 '-180,-180': 'Europe/Moscow',
7809 '-210,-270': 'Asia/Tehran',
7810 '-240,-300': 'Asia/Yerevan',
7811 '-270,-270': 'Asia/Kabul',
7812 '-300,-300': 'Asia/Tashkent',
7813 '-330,-330': 'Asia/Kolkata',
7814 '-345,-345': 'Asia/Kathmandu',
7815 '-360,-360': 'Asia/Dhaka',
7816 '-390,-390': 'Asia/Rangoon',
7817 '-420,-420': 'Asia/Bangkok',
7818 '-420,-480': 'Asia/Krasnoyarsk',
7819 '-480,-480': 'Asia/Hong_Kong',
7820 '-540,-540': 'Asia/Tokyo',
7821 '-630,-570': 'Australia/Adelaide',
7822 '-570,-570': 'Australia/Darwin',
7823 '-660,-600': 'Australia/Sydney',
7824 '-600,-600': 'Asia/Vladivostok',
7825 '-690,-690': 'Pacific/Norfolk',
7826 '-780,-720': 'Pacific/Auckland',
7827 '-825,-765': 'Pacific/Chatham',
7828 '-780,-780': 'Pacific/Tongatapu',
7829 '-840,-840': 'Pacific/Kiritimati'
7830 };
7831
7832 // *********************************************************************
7833
7834 XenForo.DatePicker = function ($input) {
7835 if (!XenForo.DatePicker.$root) {
7836 $.tools.dateinput.localize('_f',
7837 {
7838 months: XenForo.phrases._months,
7839 shortMonths: '1,2,3,4,5,6,7,8,9,10,11,12',
7840 days: 's,m,t,w,t,f,s',
7841 shortDays: XenForo.phrases._daysShort
7842 });
7843 }
7844
7845 var $date = $input.dateinput(
7846 {
7847 lang: '_f',
7848 format: 'yyyy-mm-dd', // rfc 3339 format, required by html5 date element
7849 speed: 0,
7850 yearRange: [-100, 100],
7851 onShow: function (e) {
7852 var $root = XenForo.DatePicker.$root,
7853 offset = $date.offset(),
7854 maxZIndex = 0,
7855 position = { top: offset.top + $date.outerHeight() };
7856
7857 if (XenForo.isRTL()) {
7858 position.right = $('html').width() - offset.left - $date.outerWidth();
7859 } else {
7860 position.left = offset.left;
7861 }
7862
7863 $root.css(position);
7864
7865 $date.parents().each(function (i, el) {
7866 var zIndex = parseInt($(el).css('z-index'), 10);
7867 if (zIndex > maxZIndex) {
7868 maxZIndex = zIndex;
7869 }
7870 });
7871
7872 $root.css('z-index', maxZIndex + 1000);
7873 }
7874 });
7875
7876 $date.addClass($input.attr('class'));
7877 if ($input.attr('id')) {
7878 $date.attr('id', $input.attr('id'));
7879 }
7880
7881 // this is needed to handle input[type=reset] buttons that end up focusing the field
7882 $date.closest('form').on('reset', function () {
7883 setTimeout(function () {
7884 $date.data('dateinput').hide();
7885 }, 10);
7886 setTimeout(function () {
7887 $date.data('dateinput').hide();
7888 }, 100);
7889 });
7890
7891 if (!XenForo.DatePicker.$root) {
7892 XenForo.DatePicker.$root = $('#calroot').appendTo(document.body);
7893
7894 $('#calprev').html(XenForo.isRTL() ? '→' : '←').prop('unselectable', true);
7895 $('#calnext').html(XenForo.isRTL() ? '←' : '→').prop('unselectable', true);
7896 }
7897 };
7898
7899 XenForo.DatePicker.$root = null;
7900
7901 // *********************************************************************
7902 var localStorageSmiliesKey = 'smilies_cache';
7903
7904
7905 XenForo.AutoComplete = function ($element) {
7906 this.__construct($element);
7907 };
7908 XenForo.AutoComplete.prototype =
7909 {
7910 __construct: function ($input) {
7911 this.$input = $input;
7912
7913 this.url = $input.data('acurl') || XenForo.AutoComplete.getDefaultUrl();
7914 this.extraFields = $input.data('acextrafields');
7915 this.smiliesAutoCompleteCount = 5;
7916 var options = {
7917 multiple: $input.hasClass('AcSingle') ? false : ',', // mutiple value joiner
7918 minLength: 2, // min word length before lookup
7919 queryKey: 'q',
7920 extraParams: { searchOnlyWithAtSign: $input.hasClass('AtSign') },
7921 jsonContainer: 'results',
7922 autoSubmit: XenForo.isPositive($input.data('autosubmit'))
7923 };
7924 if ($input.data('acoptions')) {
7925 options = $.extend(options, $input.data('acoptions'));
7926 }
7927
7928 if (options.autoSubmit) {
7929 options.multiple = false;
7930 }
7931
7932 this.multiple = options.multiple;
7933 this.minLength = options.minLength;
7934 this.queryKey = options.queryKey;
7935 this.extraParams = options.extraParams;
7936 this.jsonContainer = options.jsonContainer;
7937 this.autoSubmit = options.autoSubmit;
7938 this.enabled = true
7939 var self = this
7940 this.loadVal = '';
7941 this.lastAcLookup = []
7942 this.results = new XenForo.AutoCompleteResults({
7943 onInsert: $.context(this, 'addValue')
7944 });
7945 this.acSmiliesResults = new XenForo.AutoSmiliesCompleteResults({
7946 onInsert: $.context(this, 'insertAutoComplete')
7947 });
7948
7949 $input.attr('autocomplete', 'off')
7950 .keydown($.context(this, 'keystroke'))
7951 .keypress($.context(this, 'operaKeyPress'))
7952 .blur($.context(this, 'blur'));
7953
7954 $input.on('paste', function () {
7955 setTimeout(function () {
7956 $input.trigger('keydown');
7957 }, 0);
7958 });
7959
7960 $input.closest('form').submit($.context(this, 'hideResults'));
7961
7962 var smilies_loaded = false;
7963
7964 if (supports_html5_storage()) {
7965 var temp = localStorage.getItem(localStorageSmiliesKey);
7966 if (temp) {
7967 temp = JSON.parse(temp)
7968 if (temp.cache === XenForo.smiliesCacheKey) {
7969 this.smilies_complete = temp
7970 smilies_loaded = true
7971 }
7972 }
7973
7974 if (!smilies_loaded) {
7975 XenForo.ajax(
7976 XenForo._editorSmiliesUrl,
7977 {},
7978 function (ajaxData) {
7979 if (XenForo.hasResponseError(ajaxData)) {
7980 return;
7981 }
7982 var data = {}
7983 data.smilies = ajaxData.smilies
7984 data.cache = XenForo.smiliesCacheKey
7985 if (temp) {
7986 data.recently = temp.recently
7987 } else {
7988 data.recently = []
7989 }
7990 localStorage.setItem(localStorageSmiliesKey, JSON.stringify(data))
7991 self.smilies_complete = data
7992 }).complete(function () {
7993 this.smilies_complete = self.smilies_complete
7994 }.bind(this));
7995 }
7996 }
7997
7998 if (this.$input.hasClass('MessageCtrl')) {
7999 $(".ImNotification--QuickReplyForm").on('submit', function (e) {
8000 self.hideAutoComplete();
8001 })
8002 } else {
8003 $("input[type=submit]").on('click', function () {
8004 self.hideAutoComplete();
8005 })
8006 }
8007 },
8008
8009 setEnabled(enabled) {
8010 if (this.loadTimer) clearTimeout(this.loadTimer)
8011 this.loadTimer = null
8012 this.hideResults()
8013 this.enabled = enabled
8014 },
8015
8016 keystroke: function (e) {
8017 if (!this.enabled) return
8018 var code = e.keyCode || e.charCode, prevent = true;
8019
8020 switch (code) {
8021 case 40:
8022 if (this.results.isVisible()) {
8023 this.results.selectResult(1);
8024 break; // down
8025 }
8026 case 39:
8027 if (this.acSmiliesResults.isVisible()) {
8028 this.acSmiliesResults.selectResult(1);
8029 break; // right
8030 }
8031 case 38:
8032 if (this.results.isVisible()) {
8033 this.results.selectResult(-1);
8034 break; // up
8035 }
8036 case 37:
8037 if (this.acSmiliesResults.isVisible()) {
8038 this.acSmiliesResults.selectResult(-1);
8039 break; // left
8040 }
8041 case 27:
8042 if (this.results.isVisible()) {
8043 this.results.hideResults();
8044 } else {
8045 if (this.acSmiliesResults.isVisible()) {
8046 this.acSmiliesResults.hideResults()
8047 } else {
8048 prevent = false;
8049 }
8050 }
8051 break; // esc
8052 case 13: // enter
8053 if (this.results.isVisible()) {
8054 this.results.insertSelectedResult();
8055 } else {
8056 if (this.acSmiliesResults.isVisible() && this.acSmiliesResults.selectedResult !== -1) {
8057 this.acSmiliesResults.insertSelectedResult()
8058 } else {
8059 prevent = false;
8060 }
8061 }
8062 this.hideAutoComplete();
8063 break;
8064
8065 default:
8066 prevent = false;
8067 if (e.ctrlKey && code === 86 || code === 17) {
8068 return
8069 }
8070 if (this.loadTimer) {
8071 clearTimeout(this.loadTimer);
8072 }
8073 this.loadTimer = setTimeout($.context(this, 'load'), 200);
8074 this.loadTimerSmilies = setTimeout($.context(this, 'smilies'), 1);
8075 if (code != 229) {
8076 this.results.hideResults();
8077 }
8078 }
8079 if (prevent) {
8080 e.preventDefault();
8081 }
8082 this.preventKey = prevent;
8083 },
8084
8085 operaKeyPress: function (e) {
8086 if ($.browser.opera && this.preventKey) {
8087 e.preventDefault();
8088 }
8089 },
8090
8091 blur: function (e) {
8092 clearTimeout(this.loadTimer);
8093
8094 // timeout ensures that clicks still register
8095 setTimeout($.context(this, 'hideResults'), 250);
8096
8097 if (this.xhr) {
8098 this.xhr.abort();
8099 this.xhr = false;
8100 }
8101 },
8102
8103 smilies: function () {
8104 if (!this.$input.hasClass('AutoSmiliesComplete')) {
8105 return
8106 }
8107
8108 if (!(XenForo.SmiliesAutoComplete && !XenForo.SmiliesAutoComplete.enable)) {
8109 var smiliesCompleteText = this.findCurrentSmiliesAutoCompleteOption();
8110 var smiliesFullCompleteText = this.findFullSmiliesAutoCompleteOption();
8111 } else {
8112 var smiliesCompleteText = []
8113 var smiliesFullCompleteText = []
8114 }
8115
8116 if (smiliesCompleteText.length) {
8117 return this.triggerSmiliesAutoComplete(smiliesCompleteText);
8118 } else {
8119 if (smiliesFullCompleteText.length) {
8120 return this.triggerSmiliesAutoComplete(smiliesFullCompleteText, true);
8121 }
8122 }
8123 return this.hideAutoComplete();
8124
8125 },
8126
8127 hideAutoComplete: function () {
8128 this.acSmiliesResults.hideResults();
8129 },
8130
8131 load: function () {
8132 var lastLoad = this.loadVal,
8133 params = this.extraParams;
8134
8135 if (this.loadTimer) {
8136 clearTimeout(this.loadTimer);
8137 }
8138
8139 this.loadVal = this.getPartialValue();
8140
8141 if (this.loadVal === '') {
8142 this.hideResults();
8143 return;
8144 }
8145
8146 if (params.searchOnlyWithAtSign) {
8147 if (this.loadVal.indexOf('@') !== 0) {
8148 return;
8149 }
8150
8151 this.loadVal = this.loadVal.substr(1);
8152 }
8153
8154 if (this.loadVal == lastLoad) {
8155 return;
8156 }
8157
8158 if (this.loadVal.length < this.minLength) {
8159 return;
8160 }
8161
8162 if (this.loadVal.indexOf(",") !== -1) {
8163 return;
8164 }
8165
8166 params[this.queryKey] = this.loadVal;
8167
8168 if (this.extraFields != '') {
8169 $(this.extraFields).each(function () {
8170 params[this.name] = $(this).val();
8171 });
8172 }
8173
8174 if (this.xhr) {
8175 this.xhr.abort();
8176 }
8177
8178 this.xhr = XenForo.ajax(
8179 this.url,
8180 params,
8181 $.context(this, 'showResults'),
8182 { global: false, error: false }
8183 );
8184 },
8185
8186 hideResults: function () {
8187 this.results.hideResults();
8188 },
8189
8190 showResults: function (results) {
8191 if (this.xhr) {
8192 this.xhr = false;
8193 }
8194
8195 if (this.jsonContainer && results) {
8196 results = results[this.jsonContainer];
8197 }
8198
8199 this.results.showResults(this.getPartialValue(), results, this.$input);
8200 },
8201
8202 addValue: function (value) {
8203 if (this.extraParams.searchOnlyWithAtSign) {
8204 value = '@' + value;
8205 }
8206
8207 if (!this.multiple) {
8208 this.$input.val(value + ' ');
8209 } else {
8210 var values = this.getFullValues();
8211 if (value != '') {
8212 if (values.length) {
8213 value = ' ' + value;
8214 }
8215 values.push(value + this.multiple + ' ');
8216 }
8217 this.$input.val(values.join(this.multiple));
8218 }
8219 this.$input.trigger("AutoComplete", { inserted: value, current: this.$input.val() });
8220
8221 if (this.autoSubmit) {
8222 this.$input.closest('form').submit();
8223 } else {
8224 this.$input.focus();
8225 }
8226 },
8227
8228 findFullSmiliesAutoCompleteOption: function () {
8229 var selected = window.getSelection()
8230 var origin = [selected.anchorNode, selected.anchorOffset];
8231 var focus = [selected.focusNode, selected.focusOffset]
8232
8233 this.loadVal = this.getPartialValue();
8234 testText = this.loadVal
8235 var lastAt = testText.lastIndexOf(':');
8236 if (lastAt < testText.lastIndexOf('@')) {
8237 return false
8238 }
8239
8240 var list = []
8241
8242 // Search by full name
8243 list.push(...this.smilies_complete.smilies.filter(function (e) {
8244 return e.name.toUpperCase() === testText.toUpperCase() // By name
8245 || e.value.toUpperCase() === testText.toUpperCase() // By value
8246 || e.aliases.filter(function (alias) {
8247 return alias.toUpperCase() === testText.toUpperCase() // By alias
8248 }).length
8249 }
8250 ))
8251
8252 list = list.filter(function (e) { return e.value !== name.value })
8253 list.splice(this.smiliesAutoCompleteCount)
8254
8255 return list
8256 },
8257
8258 findCurrentSmiliesAutoCompleteOption: function () {
8259 var selected = window.getSelection()
8260 var origin = [selected.anchorNode, selected.anchorOffset];
8261 var focus = [selected.focusNode, selected.focusOffset]
8262
8263 this.loadVal = this.getPartialValue();
8264 testText = this.loadVal
8265 var lastAt = testText.lastIndexOf(':');
8266 if (lastAt < testText.lastIndexOf('@')) {
8267 return false
8268 }
8269
8270 var list = []
8271
8272 if (lastAt !== -1 && (lastAt === 0 || ((testText[lastAt - 1].trim() === "" || testText[lastAt + 1]) && !(/[a-zA-Z0-9]/).test(testText[lastAt - 1]))) && list.length < this.smiliesAutoCompleteCount) {
8273 var afterAt = testText.substr(lastAt + 1);
8274 afterAt = afterAt.replace(new RegExp(String.fromCharCode(160), 'g'), ' ')
8275 if (afterAt.startsWith(' ')) {
8276 return false;
8277 }
8278 if (!afterAt.trim() && this.smilies_complete.recently.length) {
8279 // Return recently
8280 list = this.smilies_complete.recently;
8281 }
8282
8283 list.push(...this.smilies_complete.smilies.filter(function (e) {
8284 return e.name.replaceAll(":", "").substr(0, afterAt.length).toUpperCase() === afterAt.toUpperCase() // By name
8285 || e.value.replaceAll(":", "").substr(0, afterAt.length).toUpperCase() === afterAt.toUpperCase() // By value
8286 || e.aliases.filter(function (alias) {
8287 return alias.replaceAll(":", "").substr(0, afterAt.length).toUpperCase() === afterAt.toUpperCase() // By alias
8288 }).length
8289 }
8290 ))
8291 }
8292
8293 list = list.filter(function (e) { return e.value !== name.value })
8294 list.splice(this.smiliesAutoCompleteCount)
8295
8296 return list
8297 },
8298
8299 insertAutoComplete: function (name) {
8300 this.$input.focus();
8301
8302 var selected = window.getSelection()
8303 var origin = [selected.anchorNode, selected.anchorOffset];
8304 var focus = [selected.focusNode, selected.focusOffset]
8305
8306 if (!focus || !origin || focus[0] != origin[0] || focus[1] != origin[1] || typeof this.smilies_complete === "undefined") {
8307 return false;
8308 }
8309
8310 var $focus = $(focus[0]),
8311 testText = focus[0].nodeType == 3 ? $focus.text().substring(0, focus[1]) : $($focus.contents().get(focus[1] - 1)).val();
8312 if (!testText.trim()) {
8313 testText = $($focus.contents().get(focus[1])).val()
8314 }
8315
8316 var lastAt = testText.toLowerCase().lastIndexOf(name[0].toLowerCase());
8317
8318 if (lastAt === -1) {
8319 lastAt = 0
8320 }
8321 var name = this.lastAcLookup[parseInt(name[1])]
8322 var value = name.value
8323 var input = this.$input.val()
8324
8325 var length = input.length
8326 input = input.substring(0, lastAt)
8327 input = input.substring(length - (lastAt + testText.length))
8328 input += value
8329 this.$input.val(input)
8330 this.$input.trigger("AutoComplete", { inserted: value, current: this.$input.val() });
8331
8332 if (this.autoSubmit) {
8333 this.$input.closest('form').submit();
8334 } else {
8335 this.$input.focus();
8336 }
8337 this.lastAcLookup = name;
8338
8339 // Save to recently
8340 var recently = this.smilies_complete.recently.filter(function (e) { return e.value !== name.value })
8341 recently.unshift(name)
8342 recently.splice(this.smiliesAutoCompleteCount)
8343 this.smilies_complete.recently = recently
8344 if (supports_html5_storage()) {
8345 localStorage.setItem(localStorageSmiliesKey, JSON.stringify(this.smilies_complete))
8346 }
8347
8348 this.hideAutoComplete();
8349 this.$input.focus();
8350 },
8351
8352 triggerSmiliesAutoComplete: function (name, def) {
8353 if (this.lastAcLookup.length && this.lastAcLookup == name) {
8354 return;
8355 }
8356
8357 if (this.$input.prop('disabled')) {
8358 return
8359 }
8360
8361 this.hideAutoComplete();
8362 this.lastAcLookup = name;
8363 this.showAutoCompleteResults({ results: name }, def)
8364 },
8365
8366 showAutoCompleteResults: function (ajaxData, def) {
8367 this.acXhr = false;
8368
8369 if (JSON.stringify(this.lastAcLookup) !== JSON.stringify(this.findCurrentSmiliesAutoCompleteOption()) && JSON.stringify(this.lastAcLookup) !== JSON.stringify(this.findFullSmiliesAutoCompleteOption())) {
8370 return;
8371 }
8372
8373 this.acSmiliesResults.showResults(
8374 this.lastAcLookup,
8375 ajaxData.results,
8376 this.$input
8377 );
8378 if (def) {
8379 this.acSmiliesResults.selectResult(-1)
8380 }
8381 },
8382
8383 getFullValues: function () {
8384 var val = this.$input.val();
8385
8386 if (val == '') {
8387 return [];
8388 }
8389
8390 if (!this.multiple) {
8391 return [val];
8392 } else {
8393 splitPos = val.lastIndexOf(this.multiple);
8394 if (splitPos == -1) {
8395 return [];
8396 } else {
8397 val = val.substr(0, splitPos);
8398 return val.split(this.multiple);
8399 }
8400 }
8401 },
8402
8403 getPartialValue: function () {
8404 var val = this.$input.val(),
8405 splitPos;
8406
8407 if (!this.multiple) {
8408 return $.trim(val);
8409 } else {
8410 splitPos = val.lastIndexOf(this.multiple);
8411 let value
8412 if (splitPos == -1) {
8413 value = $.trim(val);
8414 } else {
8415 value = $.trim(val.substr(splitPos + this.multiple.length));
8416 }
8417 return value.replace(/^@/, '')
8418 }
8419 }
8420 };
8421 XenForo.AutoComplete.getDefaultUrl = function () {
8422 if (XenForo.AutoComplete.defaultUrl === null) {
8423 if ($('html').hasClass('Admin')) {
8424 XenForo.AutoComplete.defaultUrl = 'admin.php?users/search-name&_xfResponseType=json';
8425 } else {
8426 XenForo.AutoComplete.defaultUrl = 'members/find&_xfResponseType=json';
8427 }
8428 }
8429 ;
8430
8431 return XenForo.AutoComplete.defaultUrl;
8432 };
8433 XenForo.AutoComplete.defaultUrl = null;
8434
8435 // *********************************************************************
8436
8437 XenForo.UserTagger = function ($element) {
8438 this.__construct($element);
8439 };
8440 XenForo.UserTagger.prototype =
8441 {
8442 __construct: function ($textarea) {
8443 this.$textarea = $textarea;
8444 this.url = $textarea.data('acurl') || XenForo.AutoComplete.getDefaultUrl();
8445 this.acResults = new XenForo.AutoCompleteResults({
8446 onInsert: $.context(this, 'insertAutoComplete')
8447 });
8448
8449 var self = this,
8450 hideTimer,
8451 hideCallback = function () {
8452 if (hideTimer) {
8453 return;
8454 }
8455
8456 hideTimer = setTimeout(function () {
8457 self.acResults.hideResults();
8458 hideTimer = null;
8459 }, 200);
8460 };
8461
8462 $(document).on('scroll', hideCallback);
8463
8464 $textarea.on('click blur', hideCallback);
8465 $textarea.on('keydown', function (e) {
8466 var prevent = true,
8467 acResults = self.acResults;
8468
8469 if (!acResults.isVisible()) {
8470 return;
8471 }
8472
8473 switch (e.keyCode) {
8474 case 40:
8475 acResults.selectResult(1);
8476 break; // down
8477 case 38:
8478 acResults.selectResult(-1);
8479 break; // up
8480 case 27:
8481 acResults.hideResults();
8482 break; // esc
8483 case 13:
8484 acResults.insertSelectedResult();
8485 break; // enter
8486
8487 default:
8488 prevent = false;
8489 }
8490
8491 if (prevent) {
8492 e.stopPropagation();
8493 e.stopImmediatePropagation();
8494 e.preventDefault();
8495 }
8496 });
8497 $textarea.on('keyup', function (e) {
8498 var autoCompleteText = self.findCurrentAutoCompleteOption();
8499 if (autoCompleteText) {
8500 self.triggerAutoComplete(autoCompleteText);
8501 } else {
8502 self.hideAutoComplete();
8503 }
8504 });
8505 },
8506
8507 findCurrentAutoCompleteOption: function () {
8508 var $textarea = this.$textarea;
8509
8510 $textarea.focus();
8511 var sel = $textarea.getSelection(),
8512 testText,
8513 lastAt;
8514
8515 if (!sel || sel.end <= 1) {
8516 return false;
8517 }
8518
8519 testText = $textarea.val().substring(0, sel.end);
8520 lastAt = testText.lastIndexOf('@');
8521
8522 if (lastAt != -1 && (lastAt == 0 || testText.substr(lastAt - 1, 1).match(/(\s|[\](,]|--)/))) {
8523 var afterAt = testText.substr(lastAt + 1);
8524 if (!afterAt.match(/\s/) || afterAt.length <= 10) {
8525 return afterAt;
8526 }
8527 }
8528
8529 return false;
8530 },
8531
8532 insertAutoComplete: function (name) {
8533 var $textarea = this.$textarea;
8534
8535 $textarea.focus();
8536 var sel = $textarea.getSelection(),
8537 testText;
8538
8539 if (!sel || sel.end <= 1) {
8540 return false;
8541 }
8542
8543 testText = $textarea.val().substring(0, sel.end);
8544
8545 var lastAt = testText.lastIndexOf('@');
8546 if (lastAt != -1) {
8547 $textarea.setSelection(lastAt, sel.end);
8548 $textarea.replaceSelectedText('@' + name + ' ', 'collapseToEnd');
8549 this.lastAcLookup = name + ' ';
8550 }
8551 },
8552
8553 triggerAutoComplete: function (name) {
8554 if (this.lastAcLookup && this.lastAcLookup == name) {
8555 return;
8556 }
8557
8558 this.hideAutoComplete();
8559 this.lastAcLookup = name;
8560 if (name.length > 2 && name.substr(0, 1) != '[') {
8561 this.acLoadTimer = setTimeout($.context(this, 'autoCompleteLookup'), 200);
8562 }
8563 },
8564
8565 autoCompleteLookup: function () {
8566 if (this.acXhr) {
8567 this.acXhr.abort();
8568 }
8569
8570 if (this.lastAcLookup != this.findCurrentAutoCompleteOption()) {
8571 return;
8572 }
8573
8574 this.acXhr = XenForo.ajax(
8575 this.url,
8576 { q: this.lastAcLookup },
8577 $.context(this, 'showAutoCompleteResults'),
8578 { global: false, error: false }
8579 );
8580 },
8581
8582 showAutoCompleteResults: function (ajaxData) {
8583 this.acXhr = false;
8584 this.acResults.showResults(
8585 this.lastAcLookup,
8586 ajaxData.results,
8587 this.$textarea
8588 );
8589 },
8590
8591 hideAutoComplete: function () {
8592 this.acResults.hideResults();
8593
8594 if (this.acLoadTimer) {
8595 clearTimeout(this.acLoadTimer);
8596 this.acLoadTimer = false;
8597 }
8598 }
8599 };
8600
8601 // *********************************************************************
8602
8603 XenForo.AutoCompleteResults = function (options) {
8604 this.__construct(options);
8605 };
8606 XenForo.AutoCompleteResults.prototype =
8607 {
8608 __construct: function (options) {
8609 this.options = $.extend({
8610 onInsert: false
8611 }, options);
8612
8613 this.selectedResult = 0;
8614 this.$results = false;
8615 this.resultsVisible = false;
8616 this.resizeBound = false;
8617 },
8618
8619 isVisible: function () {
8620 return this.resultsVisible;
8621 },
8622
8623 hideResults: function () {
8624 this.resultsVisible = false;
8625
8626 if (this.$results) {
8627 this.$results.hide();
8628 }
8629 },
8630
8631 showResults: function (val, results, $targetOver, cssPosition) {
8632 var maxZIndex = 0,
8633 i,
8634 filterRegex,
8635 result,
8636 $li;
8637
8638 if (!results) {
8639 this.hideResults();
8640 return;
8641 }
8642
8643 this.resultsVisible = false;
8644
8645 if (!this.$results) {
8646 this.$results = $('<ul />')
8647 .css({ position: 'absolute', display: 'none' })
8648 .addClass('autoCompleteList')
8649 .appendTo(document.body);
8650
8651 $targetOver.parents().each(function (i, el) {
8652 var $el = $(el),
8653 zIndex = parseInt($el.css('z-index'), 10);
8654
8655 if (zIndex > maxZIndex) {
8656 maxZIndex = zIndex;
8657 }
8658 });
8659
8660 this.$results.css('z-index', maxZIndex + 1000);
8661 this.$results.data('XenForo.AutoCompleteResults', this);
8662 } else {
8663 this.$results.hide().empty();
8664 }
8665
8666 //filterRegex = new RegExp('(' + XenForo.regexQuote(val) + ')', 'i');
8667
8668 for (i in results) {
8669 if (!results.hasOwnProperty(i)) {
8670 continue;
8671 }
8672
8673 result = results[i];
8674 var $html = $(result.username)
8675 $html.find('.scamNotice').remove()
8676 var $avatar = $html.clone();
8677 $avatar.prepend($(`<img class="smallCompleteAvatar" src="${result.avatar}" style="float: left;margin-top: 2px;margin-right: 5px;border-radius: 17px"/>`))
8678 var $lastSpan = $html.find('span:last')
8679 $lastSpan.text('@' + $lastSpan.text())
8680
8681 $li = $('<li />')
8682 .css('cursor', 'pointer')
8683 .attr('unselectable', 'on')
8684 .data('autocomplete', i)
8685 .data('autocompleteHtml', $html.prop('outerHTML'))
8686 .data('autocompleteAvatar', $avatar.prop('outerHTML'))
8687 .click($.context(this, 'resultClick'))
8688 .mouseenter($.context(this, 'resultMouseEnter'))
8689 .on('mousedown touchstart', function (e) {
8690 e.stopPropagation() // prevent focusing, fix for froala popups
8691 })
8692
8693 if (typeof result == 'string') {
8694 $li.html(XenForo.htmlspecialchars(result));//.replace(filterRegex, '<strong>$1</strong>'));
8695 } else {
8696 // unescaping the avatar here as we expect the username to be escaped, so this means
8697 // that we can/should expect all values to be escaped
8698 $li.html(result['username'])
8699 .prepend(
8700 $('<img class="autoCompleteAvatar" />')
8701 .attr('src', XenForo.htmlEntityDecode(result['avatar']))
8702 );
8703 /*$li.html(result['username'].replace(filterRegex, '<strong>$1</strong>'))
8704 .prepend(
8705 $('<img class="autoCompleteAvatar" />')
8706 .attr('src', XenForo.htmlEntityDecode(result['avatar']))
8707 );*/
8708
8709 }
8710
8711 $li.appendTo(this.$results);
8712 }
8713
8714
8715 if (!this.$results.find('li').length) {
8716 return;
8717 }
8718
8719 this.selectResult(0, true);
8720
8721 if (!this.resizeBound) {
8722 $(window).bind('resize', $.context(this, 'hideResults'));
8723 }
8724
8725 if (!cssPosition) {
8726 var offset = $targetOver.offset();
8727
8728 cssPosition = {
8729 top: offset.top + $targetOver.outerHeight(),
8730 left: offset.left
8731 };
8732
8733 if (XenForo.isRTL()) {
8734 cssPosition.right = $('html').width() - offset.left - $targetOver.outerWidth();
8735 cssPosition.left = 'auto';
8736 }
8737 }
8738
8739 this.$results.css(cssPosition).show();
8740 this.$results.wrapInner('<div class="scrollbar scrollbar-macosx scrollbar-dynamic"></div>');
8741 this.$results.children().scrollbar();
8742 this.resultsVisible = true;
8743
8744 $(window).on('scroll', function (e) {
8745 var offset = $targetOver.offset();
8746 cssPosition = {
8747 top: offset.top + $targetOver.outerHeight(),
8748 left: offset.left
8749 };
8750
8751 if (XenForo.isRTL()) {
8752 cssPosition.right = $('html').width() - offset.left - $targetOver.outerWidth();
8753 cssPosition.left = 'auto';
8754 }
8755 this.$results.css(cssPosition)
8756 var rect = this.$results.get(0).getBoundingClientRect();
8757 if (!(rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth))) {
8758 if (!cssPosition.focus) {
8759 cssPosition.focus = 40;
8760 }
8761 cssPosition.top -= this.$results.get(0).offsetHeight;
8762 cssPosition.top -= cssPosition.focus;
8763 if (cssPosition.top < 0 && XenForo.isTouchBrowser()) return;
8764 this.$results.css(cssPosition);
8765 }
8766 }.bind(this));
8767
8768 var rect = this.$results.get(0).getBoundingClientRect();
8769 if (!(rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth))) {
8770 if (!cssPosition.focus) {
8771 cssPosition.focus = 40;
8772 }
8773 cssPosition.top -= this.$results.get(0).offsetHeight;
8774 cssPosition.top -= cssPosition.focus;
8775 if (cssPosition.top < 0 && XenForo.isTouchBrowser()) return;
8776 this.$results.css(cssPosition);
8777 }
8778 },
8779
8780 resultClick: function (e) {
8781 e.stopPropagation();
8782
8783 this.insertResult($(e.currentTarget).data('autocomplete'), $(e.currentTarget).data('autocompleteHtml'), $(e.currentTarget).data('autocompleteAvatar'));
8784 this.hideResults();
8785 },
8786
8787 resultMouseEnter: function (e) {
8788 this.selectResult($(e.currentTarget).index(), true);
8789 },
8790
8791 selectResult: function (shift, absolute) {
8792 var sel, children;
8793
8794 if (!this.$results) {
8795 return;
8796 }
8797
8798 if (absolute) {
8799 this.selectedResult = shift;
8800 } else {
8801 this.selectedResult += shift;
8802 }
8803
8804 sel = this.selectedResult;
8805 children = this.$results.find('li');
8806 children.each(function (i) {
8807 if (i == sel) {
8808 $(this).addClass('selected');
8809 } else {
8810 $(this).removeClass('selected');
8811 }
8812 });
8813
8814 if (sel < 0 || sel >= children.length) {
8815 this.selectedResult = -1;
8816 }
8817 },
8818
8819 insertSelectedResult: function () {
8820 var res, ret = false;
8821
8822 if (!this.resultsVisible) {
8823 return false;
8824 }
8825
8826 if (this.selectedResult >= 0) {
8827 res = this.$results.find('li').get(this.selectedResult);
8828 if (res) {
8829 this.insertResult($(res).data('autocomplete'), $(res).data('autocompleteHtml'), $(res).data('autocompleteAvatar'));
8830 ret = true;
8831 }
8832 }
8833
8834 this.hideResults();
8835
8836 return ret;
8837 },
8838
8839 insertResult: function (value, htmlValue, avatarValue) {
8840 if (this.options.onInsert) {
8841 this.options.onInsert(value, htmlValue, avatarValue);
8842 }
8843 }
8844 };
8845
8846 XenForo.AutoSmiliesCompleteResults = function (options) { this.__construct(options); };
8847 XenForo.AutoSmiliesCompleteResults.prototype =
8848 {
8849 __construct: function (options) {
8850 this.options = $.extend({
8851 onInsert: false
8852 }, options);
8853
8854 this.selectedResult = 0;
8855 this.$results = false;
8856 this.resultsVisible = false;
8857 this.resizeBound = false;
8858 },
8859
8860 isVisible: function () {
8861 return this.resultsVisible;
8862 },
8863
8864 hideResults: function () {
8865 this.resultsVisible = false;
8866
8867 if (this.$results) {
8868 this.$results.hide();
8869 }
8870 },
8871
8872 showResults: function (val, results, $targetOver, cssPosition) {
8873 var maxZIndex = 0,
8874 i,
8875 filterRegex,
8876 result,
8877 $li;
8878
8879 if (!results) {
8880 this.hideResults();
8881 return;
8882 }
8883
8884 this.resultsVisible = false;
8885
8886 if (!this.$results) {
8887 this.$results = $('<ul />')
8888 .css({
8889 position: 'absolute', display: 'none',
8890 'background': 'rgb(45, 45, 45)', 'font-size': '13px', 'border-radius': '3px'
8891 })
8892 .addClass('autoCompleteListSmilies')
8893 .appendTo(document.body);
8894
8895 $targetOver.parents().each(function (i, el) {
8896 var $el = $(el),
8897 zIndex = parseInt($el.css('z-index'), 10);
8898
8899 if (zIndex > maxZIndex) {
8900 maxZIndex = zIndex;
8901 }
8902 });
8903
8904 this.$results.css('z-index', maxZIndex + 1000);
8905 this.$results.data('XenForo.AutoSmiliesCompleteResults', this);
8906 }
8907 else {
8908 this.$results.hide().empty();
8909 }
8910
8911 filterRegex = new RegExp('(' + XenForo.regexQuote(val) + ')', 'i');
8912
8913 for (var i = 0; i < results.length; i++) {
8914 if (!results.hasOwnProperty(i)) {
8915 continue;
8916 }
8917
8918 result = results[i];
8919
8920 $li = $('<li />')
8921 .css('cursor', 'pointer')
8922 .css('float', 'left')
8923 .css('box-sizing', 'border-box')
8924 .css('line-height', '15px')
8925 .css("display", "flex")
8926 .css("justify-content", 'center')
8927 .css('width', '50px')
8928 .css('height', '50px')
8929 .attr('unselectable', 'on')
8930 .data('autocomplete', i)
8931 .click($.context(this, 'resultClick'))
8932 .mouseenter($.context(this, 'resultMouseEnter'));
8933
8934 $listElement = $('<li />', { 'class': 'Smilie' }).css('display', 'flex').css('align-items', 'center')
8935 $listElement.append($('<img />', { 'src': result.image, title: result.name, alt: result.value, 'data-smilie': 'yes' }).css('max-width', '46px'))
8936 $li.html($listElement.get(0).outerHTML)
8937 $li.appendTo(this.$results);
8938 }
8939
8940 if (!this.$results.children().length) {
8941 return;
8942 }
8943
8944 this.selectResult(0, true);
8945
8946 if (!this.resizeBound) {
8947 $(window).bind('resize', $.context(this, 'hideResults'));
8948 }
8949
8950 if (!cssPosition) {
8951 var offset = $targetOver.offset();
8952
8953 cssPosition = {
8954 top: offset.top + $targetOver.outerHeight(),
8955 left: offset.left
8956 };
8957
8958 if (XenForo.isRTL()) {
8959 cssPosition.right = $('html').width() - offset.left - $targetOver.outerWidth();
8960 cssPosition.left = 'auto';
8961 }
8962 }
8963 this.$results.css(cssPosition);
8964 this.$results.show()
8965 if (!isElementInViewport(this.$results)) {
8966 if (!cssPosition.focus) {
8967 cssPosition.focus = 40
8968 }
8969 cssPosition.top -= this.$results.get(0).offsetHeight
8970 cssPosition.top -= cssPosition.focus
8971 this.$results.css(cssPosition);
8972 }
8973 $(window).on('scroll', function (e) {
8974 var offset = $targetOver.offset();
8975 cssPosition = {
8976 top: offset.top + $targetOver.outerHeight(),
8977 left: offset.left
8978 };
8979
8980 if (XenForo.isRTL()) {
8981 cssPosition.right = $('html').width() - offset.left - $targetOver.outerWidth();
8982 cssPosition.left = 'auto';
8983 }
8984 this.$results.css(cssPosition)
8985 var rect = this.$results.get(0).getBoundingClientRect();
8986 if (!(rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth))) {
8987 if (!cssPosition.focus) {
8988 cssPosition.focus = 40
8989 }
8990 cssPosition.top -= this.$results.get(0).offsetHeight
8991 cssPosition.top -= cssPosition.focus
8992 this.$results.css(cssPosition);
8993 }
8994 }.bind(this));
8995 this.resultsVisible = true;
8996 },
8997
8998 resultClick: function (e) {
8999 e.stopPropagation();
9000
9001 this.insertResult($(e.currentTarget).data('autocomplete'));
9002 this.hideResults();
9003 },
9004
9005 resultMouseEnter: function (e) {
9006 this.selectResult($(e.currentTarget).index(), true);
9007 },
9008
9009 selectResult: function (shift, absolute) {
9010 var sel, children;
9011
9012 if (!this.$results) {
9013 return;
9014 }
9015
9016 if (absolute) {
9017 this.selectedResult = shift;
9018 }
9019 else {
9020 this.selectedResult += shift;
9021 }
9022
9023 sel = this.selectedResult;
9024 children = this.$results.children();
9025
9026 if (sel >= children.length) {
9027 this.selectedResult = 0;
9028 sel = 0;
9029 }
9030
9031 if (sel < -1) {
9032 return this.hideResults()
9033 }
9034
9035 children.each(function (i) {
9036 if (i == sel) {
9037 $(this).css('background', 'rgb(54, 54, 54)')
9038 $(this).addClass('selected');
9039 }
9040 else {
9041 $(this).css('background', 'rgb(45, 45, 45)')
9042 $(this).removeClass('selected');
9043 }
9044 });
9045
9046 },
9047
9048 insertSelectedResult: function () {
9049 var res, ret = false;
9050
9051 if (!this.resultsVisible) {
9052 return false;
9053 }
9054
9055 if (this.selectedResult >= 0) {
9056 res = this.$results.children().get(this.selectedResult);
9057 if (res) {
9058 this.insertResult($(res).data('autocomplete'));
9059 ret = true;
9060 }
9061 }
9062
9063 this.hideResults();
9064
9065 return ret;
9066 },
9067
9068 insertResult: function (value) {
9069 if (this.options.onInsert) {
9070 this.options.onInsert(":" + value);
9071 }
9072 }
9073 };
9074
9075 XenForo.AutoTemplateCompleteResults = function (options) { this.__construct(options); };
9076 XenForo.AutoTemplateCompleteResults.prototype =
9077 {
9078 __construct: function (options) {
9079 this.options = $.extend({
9080 onInsert: false
9081 }, options);
9082
9083 this.selectedResult = 0;
9084 this.$results = false;
9085 this.resultsVisible = false;
9086 this.resizeBound = false;
9087 },
9088
9089 isVisible: function () {
9090 return this.resultsVisible;
9091 },
9092
9093 hideResults: function () {
9094 this.resultsVisible = false;
9095
9096 if (this.$results) {
9097 this.$results.hide();
9098 }
9099 },
9100
9101 showResults: function (val, results, $targetOver, cssPosition) {
9102 var maxZIndex = 0,
9103 i,
9104 filterRegex,
9105 result,
9106 $li;
9107
9108 if (!results) {
9109 this.hideResults();
9110 return;
9111 }
9112
9113 this.resultsVisible = false;
9114
9115 if (!this.$results) {
9116 this.$results = $('<ul />')
9117 .css({ position: 'absolute', display: 'none' })
9118 .addClass('autoCompleteList')
9119 .appendTo(document.body);
9120
9121 $targetOver.parents().each(function (i, el) {
9122 var $el = $(el),
9123 zIndex = parseInt($el.css('z-index'), 10);
9124
9125 if (zIndex > maxZIndex) {
9126 maxZIndex = zIndex;
9127 }
9128 });
9129
9130 this.$results.css('z-index', maxZIndex + 1000);
9131 }
9132 else {
9133 this.$results.hide().empty();
9134 }
9135
9136 filterRegex = new RegExp('(' + XenForo.regexQuote(val) + ')', 'i');
9137
9138 for (var i = 0; i < results.length; i++) {
9139 if (!results.hasOwnProperty(i)) {
9140 continue;
9141 }
9142
9143 result = results[i];
9144
9145 $li = $('<li />')
9146 .css('cursor', 'pointer')
9147 .attr('unselectable', 'on')
9148 .data('autocomplete', i)
9149 .click($.context(this, 'resultClick'))
9150 .mouseenter($.context(this, 'resultMouseEnter'));
9151
9152 $listElement = $('<div />', { 'class': 'InsertTemplate' })
9153 $listElement.append($('<div />', { 'class': 'title bold', text: result['title'] }))
9154 $listElement.append($('<div />', { 'class': 'content', text: result['content'] }))
9155 $listElement.append($('<div />', { 'class': 'ContentHtml dnone', text: result['text'] }))
9156 $li.html($listElement.get(0).outerHTML)
9157 $li.appendTo(this.$results);
9158 }
9159
9160 if (!this.$results.children().length) {
9161 return;
9162 }
9163
9164 this.selectResult(0, true);
9165
9166 if (!this.resizeBound) {
9167 $(window).bind('resize', $.context(this, 'hideResults'));
9168 }
9169
9170 if (!cssPosition) {
9171 var offset = $targetOver.offset();
9172
9173 cssPosition = {
9174 top: offset.top + $targetOver.outerHeight(),
9175 left: offset.left
9176 };
9177
9178 if (XenForo.isRTL()) {
9179 cssPosition.right = $('html').width() - offset.left - $targetOver.outerWidth();
9180 cssPosition.left = 'auto';
9181 }
9182 }
9183 this.$results.css(cssPosition);
9184 this.$results.show()
9185 if (!isElementInViewport(this.$results)) {
9186 if (!cssPosition.focus) {
9187 cssPosition.focus = 40
9188 }
9189 cssPosition.top -= this.$results.get(0).offsetHeight
9190 cssPosition.top -= cssPosition.focus
9191 this.$results.css(cssPosition);
9192 }
9193 $(window).on('scroll', function (e) {
9194 var offset = $targetOver.offset();
9195 cssPosition = {
9196 top: offset.top + $targetOver.outerHeight(),
9197 left: offset.left
9198 };
9199
9200 if (XenForo.isRTL()) {
9201 cssPosition.right = $('html').width() - offset.left - $targetOver.outerWidth();
9202 cssPosition.left = 'auto';
9203 }
9204 this.$results.css(cssPosition)
9205 var rect = this.$results.get(0).getBoundingClientRect();
9206 if (!(rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth))) {
9207 if (!cssPosition.focus) {
9208 cssPosition.focus = 40
9209 }
9210 cssPosition.top -= this.$results.get(0).offsetHeight
9211 cssPosition.top -= cssPosition.focus
9212 this.$results.css(cssPosition);
9213 }
9214 }.bind(this));
9215 this.resultsVisible = true;
9216 },
9217
9218 resultClick: function (e) {
9219 e.stopPropagation();
9220
9221 this.insertResult($(e.currentTarget).data('autocomplete'));
9222 this.hideResults();
9223 },
9224
9225 resultMouseEnter: function (e) {
9226 this.selectResult($(e.currentTarget).index(), true);
9227 },
9228
9229 selectResult: function (shift, absolute) {
9230 var sel, children;
9231
9232 if (!this.$results) {
9233 return;
9234 }
9235
9236 if (absolute) {
9237 this.selectedResult = shift;
9238 }
9239 else {
9240 this.selectedResult += shift;
9241 }
9242
9243 sel = this.selectedResult;
9244 children = this.$results.children();
9245
9246 if (sel < 0 || sel >= children.length) {
9247 this.selectedResult = 0;
9248 sel = 0;
9249 }
9250
9251 children.each(function (i) {
9252 if (i == sel) {
9253 $(this).addClass('selected');
9254 }
9255 else {
9256 $(this).removeClass('selected');
9257 }
9258 });
9259 },
9260
9261 insertSelectedResult: function () {
9262 var res, ret = false;
9263
9264 if (!this.resultsVisible) {
9265 return false;
9266 }
9267
9268 if (this.selectedResult >= 0) {
9269 res = this.$results.children().get(this.selectedResult);
9270 if (res) {
9271 this.insertResult($(res).data('autocomplete'));
9272 ret = true;
9273 }
9274 }
9275
9276 this.hideResults();
9277
9278 return ret;
9279 },
9280
9281 insertResult: function (value) {
9282 if (this.options.onInsert) {
9283 this.options.onInsert(value);
9284 }
9285 }
9286 };
9287
9288 // *********************************************************************
9289
9290 XenForo.AutoSelect = function ($input) {
9291 $input.bind('focus', function (e) {
9292 setTimeout(function () {
9293 $input.select();
9294 }, 50);
9295 });
9296 };
9297
9298 // *********************************************************************
9299
9300 /**
9301 * Status Editor
9302 *
9303 * @param jQuery $textarea.StatusEditor
9304 */
9305 XenForo.StatusEditor = function ($input) {
9306 this.__construct($input);
9307 };
9308 XenForo.StatusEditor.prototype =
9309 {
9310 __construct: function ($input) {
9311 this.$input = $input
9312 .keyup($.context(this, 'update'))
9313 .keydown($.context(this, 'preventNewline'));
9314
9315 this.$counter = $(this.$input.data('statuseditorcounter'));
9316 if (!this.$counter.length) {
9317 this.$counter = $('<span />').insertAfter(this.$input);
9318 }
9319 this.$counter
9320 .addClass('statusEditorCounter')
9321 .text('0');
9322
9323 this.$form = this.$input.closest('form').bind(
9324 {
9325 AutoValidationComplete: $.context(this, 'saveStatus')
9326 });
9327
9328 this.charLimit = 140; // Twitter max characters
9329 this.charCount = 0; // number of chars currently in use
9330
9331 this.update();
9332 },
9333
9334 /**
9335 * Handles key events on the status editor, updates the 'characters remaining' output.
9336 *
9337 * @param Event e
9338 */
9339 update: function (e) {
9340 var statusText = this.$input.val();
9341
9342 if (this.$input.attr('placeholder') && this.$input.attr('placeholder') == statusText) {
9343 this.setCounterValue(this.charLimit, statusText.length);
9344 } else {
9345 this.setCounterValue(this.charLimit - statusText.length, statusText.length);
9346 }
9347 },
9348
9349 /**
9350 * Sets the value of the character countdown, and appropriate classes for that value.
9351 *
9352 * @param integer Characters remaining
9353 * @param integer Current length of status text
9354 */
9355 setCounterValue: function (remaining, length) {
9356 if (remaining < 0) {
9357 this.$counter.addClass('error');
9358 this.$counter.removeClass('warning');
9359 } else if (remaining <= this.charLimit - 130) {
9360 this.$counter.removeClass('error');
9361 this.$counter.addClass('warning');
9362 } else {
9363 this.$counter.removeClass('error');
9364 this.$counter.removeClass('warning');
9365 }
9366
9367 this.$counter.text(remaining);
9368 this.charCount = length || this.$input.val().length;
9369 },
9370
9371 /**
9372 * Don't allow newline characters in the status message.
9373 *
9374 * Submit the form if [Enter] or [Return] is hit.
9375 *
9376 * @param Event e
9377 */
9378 preventNewline: function (e) {
9379 if (e.which == 13) // return / enter
9380 {
9381 e.preventDefault();
9382
9383 $(this.$input.get(0).form).submit();
9384
9385 return false;
9386 }
9387 },
9388
9389 /**
9390 * Updates the status field after saving
9391 *
9392 * @param event e
9393 */
9394 saveStatus: function (e) {
9395 this.$input.val('');
9396 this.update(e);
9397
9398 if (e.ajaxData && e.ajaxData.status !== undefined) {
9399 $('.CurrentStatus').text(e.ajaxData.status);
9400 }
9401 }
9402 };
9403
9404 // *********************************************************************
9405
9406 window.addEventListener("scroll", function () {
9407 window.lastScrollTime = new Date().getTime()
9408 });
9409 function is_scrolling() {
9410 return window.lastScrollTime && new Date().getTime() < window.lastScrollTime + 500
9411 }
9412
9413 XenForo._ShowPreviewTimeout = null;
9414 XenForo._ActivePreviewTooltip = null;
9415
9416 XenForo.PreviewTooltip = function ($el, timeout, appendTo) {
9417 var previewUrl;
9418
9419 if (!parseInt(XenForo._enableOverlays)) {
9420 return;
9421 }
9422
9423 if (!(previewUrl = $el.data('previewurl'))) {
9424 console.warn('Preview tooltip has no preview: %o', $el);
9425 return;
9426 }
9427
9428 $el.find('[title]').andSelf().attr('title', '');
9429 var loaded = false;
9430
9431 tippy($el.get(), {
9432 touch: false,
9433 interactive: false,
9434 arrow: true,
9435 theme: timeout ? 'popup' : 'popup PreviewTooltip',
9436 animation: 'shift-toward',
9437 distance: 5,
9438 appendTo: appendTo || document.body,
9439 delay: [timeout || 300, 0],
9440 maxWidth: 400,
9441 placement: 'top-start',
9442 flipOnUpdate: true,
9443 content: '',
9444 popperOptions: {
9445 modifiers: {
9446 computeStyle: {
9447 gpuAcceleration: false
9448 }
9449 }
9450 },
9451 onShow(instance) {
9452
9453 if (is_scrolling()) {
9454 clearTimeout(XenForo._ShowPreviewTimeout);
9455 XenForo._ShowPreviewTimeout = setTimeout(function () {
9456 console.log('check scroll', is_scrolling());
9457 if (!is_scrolling()) {
9458 console.log('trigger hover', $el[0]._tippy);
9459 $el[0]._tippy.show();
9460 }
9461 }, 700);
9462
9463 return false;
9464 }
9465
9466 if (XenForo._ActivePreviewTooltip && XenForo._ActivePreviewTooltip !== instance) {
9467 XenForo._ActivePreviewTooltip.hide();
9468 }
9469
9470 if (!loaded) {
9471 XenForo.ajax(previewUrl, {}, function (ajaxData) {
9472 var $content = $('#PreviewTooltip').clone();
9473 $content.find('.previewContent').html('');
9474 //$content.hide();
9475 $(ajaxData.templateHtml).xfInsert('appendTo', $content.find('.previewContent'), 'fadeIn', 50, function () {
9476 instance.setContent($content.html());
9477 loaded = true;
9478 if ($el.is(':hover')) {
9479 instance.show();
9480 XenForo._ActivePreviewTooltip = instance;
9481 return true;
9482 }
9483
9484 });
9485 });
9486
9487 return false;
9488 }
9489
9490 return true;
9491 },
9492 })
9493 };
9494
9495 // *********************************************************************
9496
9497 /**
9498 * Allows an entire block to act as a link in the navigation popups
9499 *
9500 * @param jQuery li.PopupItemLink
9501 */
9502 XenForo.PopupItemLink = function ($listItem) {
9503 var href = $listItem.find('.PopupItemLink').first().attr('href');
9504
9505 if (href) {
9506 $listItem
9507 .addClass('PopupItemLinkActive')
9508 .on('click auxclick contextmenu', function (e) {
9509 if ($(e.target).closest('a').length || $(e.target).closest('input').length || window.getSelection().toString() !== "" || $(e.target).closest('.marketIndexItem--otherInfo').length || $(e.target).closest('.marketItemView--loginData').length) {
9510 return;
9511 }
9512
9513 switch (e.which) {
9514 case 1: XenForo.redirect(href); break
9515 case 2: window.open(href)
9516 }
9517
9518 });
9519 }
9520 };
9521
9522 // *********************************************************************
9523
9524 /**
9525 * Allows a link or input to load content via AJAX and insert it into the DOM.
9526 * The control element to which this is applied must have href or data-href attributes
9527 * and a data-target attribute describing a jQuery selector for the element relative to which
9528 * the content will be inserted.
9529 *
9530 * You may optionally provide a data-method attribute to override the default insertion method
9531 * of 'appendTo'.
9532 *
9533 * By default, the control will be unlinked and have its click event unbound after a single use.
9534 * Specify data-unlink="false" to prevent this default behaviour.
9535 *
9536 * Upon successful return of AJAX data, the control element will fire a 'ContentLoaded' event,
9537 * including ajaxData and textStatus data properties.
9538 */
9539 XenForo.Loader = function ($link) {
9540 var clickHandler = function (e) {
9541 var href = $link.attr('href') || $link.data('href'),
9542 target = $link.data('target');
9543
9544 if (href && $(target).length) {
9545 if ($link.closest('a').length) {
9546 e.stopPropagation();
9547 }
9548
9549 e.preventDefault();
9550
9551 if ($link.data('tooltip')) {
9552 $link.data('tooltip').hide();
9553 }
9554
9555 XenForo.ajax(href, {}, function (ajaxData, textStatus) {
9556 if (XenForo.hasResponseError(ajaxData)) {
9557 return false;
9558 }
9559
9560 var insertEvent = new $.Event('ContentLoaded');
9561 insertEvent.ajaxData = ajaxData;
9562 insertEvent.textStatus = textStatus;
9563
9564 $link.trigger(insertEvent);
9565
9566 if (!insertEvent.isDefaultPrevented()) {
9567 if (ajaxData.templateHtml) {
9568 new XenForo.ExtLoader(ajaxData, function () {
9569 var method = $link.data('method');
9570
9571 if (typeof $.fn[method] != 'function') {
9572 method = 'appendTo';
9573 }
9574
9575 if (method == 'replaceAll') {
9576 $(ajaxData.templateHtml).xfInsert(method, target, 'show', 0);
9577 } else {
9578 $(ajaxData.templateHtml).xfInsert(method, target);
9579 }
9580
9581 if ($link.data('unlink') !== false) {
9582 $link.removeAttr('href').removeData('href').unbind('click', clickHandler);
9583 }
9584 });
9585 }
9586 }
9587 });
9588 }
9589 };
9590
9591 $link.bind('click', clickHandler);
9592 };
9593
9594 // *********************************************************************
9595
9596 /**
9597 * Allows a control to create a clone of an existing field, like 'add new response' for polls
9598 *
9599 * @param jQuery $button.FieldAdder[data-source=#selectorOfCloneSource]
9600 */
9601 XenForo.FieldAdder = function ($button) {
9602 $($button.data('source')).filter('.PollNonJsInput').remove();
9603
9604 var maxFields = $button.data('maxfields');
9605
9606 var checkRemoveButton = function (instant) {
9607 if (maxFields && $($button.data('source')).length >= maxFields) {
9608 if (instant) {
9609 $button.css('display', 'none');
9610 } else {
9611 $button.xfHide();
9612 }
9613 } else {
9614 $button.css('display', '').xfShow();
9615 }
9616 };
9617
9618 // https://zelenka.guru/threads/4615364/
9619 var $source = $($button.data('source'))
9620 var $template = $source.last().clone()
9621 var $insertTarget = $('<div>').hide().insertAfter($source.last())
9622
9623 $button.click(function (e) {
9624 if ((!maxFields || ($($button.data('source')).length < maxFields))) {
9625 var $clone = $template.clone();
9626 $clone.find('span.button').on('click', function () {
9627 checkRemoveButton()
9628 })
9629 $clone.find('input:not([type="button"], [type="submit"])').val('').prop('disabled', true);
9630 $clone.find('.spinBoxButton').remove();
9631 $button.trigger({
9632 type: 'FieldAdderClone',
9633 clone: $clone
9634 });
9635 $clone.xfInsert('insertBefore', $insertTarget, false, false, function () {
9636 checkRemoveButton();
9637 var $inputs = $clone.find('input');
9638 $inputs.prop('disabled', false);
9639 $inputs.first().focus().select();
9640 });
9641 } else {
9642 checkRemoveButton();
9643 }
9644 });
9645
9646 checkRemoveButton(true);
9647 };
9648
9649 // *********************************************************************
9650
9651 /**
9652 * Quick way to toggle the read status of an item
9653 *
9654 * @param jQuery a.ReadToggle
9655 */
9656 XenForo.ReadToggle = function ($link) {
9657 $link.click(function (e) {
9658 e.preventDefault();
9659
9660 var xhr = null,
9661 $items = null,
9662 counterId = $link.data('counter');
9663
9664 if (xhr == null) {
9665 $items = $link.closest('.discussionListItem').andSelf().toggleClass('unread');
9666
9667 xhr = XenForo.ajax($link.attr('href'), { _xfConfirm: 1 }, function (ajaxData, textStatus) {
9668 xhr = null;
9669
9670 if (XenForo.hasResponseError(ajaxData)) {
9671 return false;
9672 }
9673
9674 if (typeof ajaxData.unread != 'undefined') {
9675 $items[(ajaxData.unread ? 'addClass' : 'removeClass')]('unread');
9676 }
9677
9678 if (counterId && typeof ajaxData.counterFormatted != 'undefined') {
9679 var $counter = $(counterId),
9680 $total = $counter.find('span.Total');
9681
9682 if ($total.length) {
9683 $total.text(ajaxData.counterFormatted);
9684 } else {
9685 $counter.text(ajaxData.counterFormatted);
9686 }
9687 }
9688
9689 if (typeof ajaxData.actionPhrase != 'undefined') {
9690 if ($link.text() != '') {
9691 $link.html(ajaxData.actionPhrase);
9692 }
9693 if ($link.attr('title')) {
9694 $link.attr('title', ajaxData.actionPhrase);
9695 }
9696 }
9697
9698 XenForo.alert(ajaxData._redirectMessage, '', 1000);
9699 });
9700 }
9701 });
9702 };
9703
9704 // *********************************************************************
9705
9706 XenForo.Notices = function ($notices) {
9707 var useCookie = XenForo.visitor.user_id ? false : true,
9708 cookieName = 'notice_dismiss',
9709 useScroller = $notices.hasClass('PanelScroller');
9710
9711 var getCookieIds = function () {
9712 var cookieValue = $.getCookie(cookieName),
9713 cookieDismissed = cookieValue ? cookieValue.split(',') : [],
9714 values = [],
9715 id;
9716
9717 for (var i = 0; i < cookieDismissed.length; i++) {
9718 id = parseInt(cookieDismissed[i], 10);
9719 if (id) {
9720 values.push(id);
9721 }
9722 }
9723
9724 return values;
9725 };
9726 var dismissed = getCookieIds();
9727
9728 if (useCookie) {
9729 $notices.find('.Notice').each(function () {
9730 var $notice = $(this),
9731 id = parseInt($notice.data('notice'), 10);
9732
9733 if (id && $.inArray(id, dismissed) != -1) {
9734 $notice.remove();
9735 $('#n' + id).remove();
9736 }
9737 });
9738 }
9739
9740 var setNoticeVisibility = function ($notices, useScroller) {
9741 if (useScroller) {
9742 $notices.find('.Notice').each(function () {
9743 var $notice = $(this),
9744 display = $(this).css('display'),
9745 id = parseInt($notice.data('notice'), 10);
9746
9747 if (display == 'none') {
9748 $notice.remove();
9749 $('#n' + id).remove();
9750 }
9751 });
9752
9753 var $navLinks = $notices.find('.Nav a');
9754 if (!$navLinks.filter('.current').length) {
9755 $navLinks.first().addClass('current');
9756 }
9757 }
9758
9759 if (!$notices.find('.Notice').length) {
9760 $notices.remove();
9761 return;
9762 }
9763 };
9764
9765 setNoticeVisibility($notices, useScroller);
9766
9767 $notices.show();
9768
9769 var PanelScroller;
9770
9771 if (useScroller) {
9772 PanelScroller = XenForo.PanelScroller($notices.find('.PanelContainer'), {
9773 scrollable: {
9774 speed: $notices.dataOrDefault('speed', 400) * XenForo._animationSpeedMultiplier,
9775 vertical: XenForo.isPositive($notices.data('vertical')),
9776 keyboard: false,
9777 touch: false,
9778 prev: '.NoticePrev',
9779 next: '.NoticeNext'
9780 },
9781 autoscroll: { interval: $notices.dataOrDefault('interval', 2000) }
9782 });
9783
9784 if (PanelScroller && PanelScroller.getItems().length > 1) {
9785 $(document).bind({
9786 XenForoWindowBlur: function (e) {
9787 PanelScroller.stop();
9788 },
9789 XenForoWindowFocus: function (e) {
9790 PanelScroller.play();
9791 }
9792 });
9793 }
9794 }
9795
9796 if ($notices.hasClass('FloatingContainer')) {
9797 var $floatingNotices = $notices.find('.Notice');
9798
9799 $floatingNotices.each(function () {
9800 var $self = $(this),
9801 displayDuration = $self.data('display-duration'),
9802 delayDuration = $self.data('delay-duration'),
9803 autoDismiss = XenForo.isPositive($self.data('auto-dismiss'));
9804
9805 if (delayDuration) {
9806 setTimeout(function () {
9807 $self.xfFadeDown(XenForo.speed.normal, function () {
9808 if (displayDuration) {
9809 setTimeout(function () {
9810 $self.xfFadeUp(XenForo.speed.normal);
9811
9812 if (autoDismiss) {
9813 $self.find('a.DismissCtrl').trigger('click');
9814 }
9815 }, displayDuration);
9816 }
9817 });
9818 }, delayDuration);
9819 } else {
9820 $self.css('display', 'block');
9821 if (displayDuration) {
9822 setTimeout(function () {
9823 $self.xfFadeUp(XenForo.speed.normal);
9824
9825 if (autoDismiss) {
9826 $self.find('a.DismissCtrl').trigger('click');
9827 }
9828 }, displayDuration);
9829 }
9830 }
9831
9832
9833 });
9834 }
9835
9836 var $cookieNotice = $notices.find('.Notice[data-notice="-1"]');
9837 if ($cookieNotice.length) {
9838 var height = $cookieNotice.height();
9839 $(document).find('footer').css('margin-bottom', height);
9840 }
9841
9842
9843 $notices.delegate('a.DismissCtrl, a.CustomDismissCtrl', 'click', function (e) {
9844 e.preventDefault();
9845
9846 var $ctrl = $(this),
9847 $notice = $ctrl.closest('.Notice'),
9848 $noticeParent = $notice.closest('.Notices');
9849
9850 if ($ctrl.data('tooltip')) {
9851 $ctrl.data('tooltip').hide();
9852 }
9853
9854 if (PanelScroller) {
9855 PanelScroller.removeItem($notice);
9856
9857 if (!PanelScroller.getItems().length) {
9858 $notices.xfFadeUp();
9859 }
9860 } else {
9861 $notice.xfFadeUp(XenForo.speed.fast, function () {
9862 if ($notice.data('notice') == '-1') {
9863 $(document).find('footer').css('margin-bottom', '');
9864 }
9865 $notice.remove();
9866 if (!$noticeParent.find('.Notice').length) {
9867 $notices.xfFadeUp();
9868 }
9869 });
9870 }
9871
9872 if (useCookie) {
9873 var noticeId = parseInt($notice.data('notice'), 10),
9874 dismissed = getCookieIds();
9875
9876 if (noticeId && $.inArray(noticeId, dismissed) == -1) {
9877 dismissed.push(noticeId);
9878 dismissed.sort(function (a, b) {
9879 return (a - b);
9880 });
9881
9882 $.setCookie(cookieName, dismissed.join(','));
9883 }
9884 } else if (!$ctrl.data('xhr')) {
9885 $ctrl.data('xhr', XenForo.ajax($ctrl.attr('href'), { _xfConfirm: 1 }, function (ajaxData, textStatus) {
9886 $ctrl.removeData('xhr');
9887
9888 if (XenForo.hasResponseError(ajaxData)) {
9889 return false;
9890 }
9891
9892 //XenForo.alert(ajaxData._redirectMessage, '', 2000);
9893 }));
9894 }
9895 });
9896
9897 $(window).on('resize', function (e) {
9898 setNoticeVisibility($notices, useScroller);
9899 });
9900 };
9901
9902 // *********************************************************************
9903
9904 XenForo.PanelScroller = function ($container, options) {
9905 var $items = $container.find('.Panels > *');
9906
9907 // don't initialize if we have just a single panel
9908 if ($items.length < 2) {
9909 $container.find('.Panels').css('position', 'static');
9910 return false;
9911 }
9912
9913 $items.find('script').remove(); // script should already have been run and document.writes break stuff
9914
9915 function resizeItems() {
9916 var maxHeight = 0;
9917
9918 $container.find('.Panels > *').css({ width: $container.innerWidth(), height: 'auto' }).each(function () {
9919 maxHeight = Math.max($(this).outerHeight(), maxHeight);
9920
9921 }).andSelf().css('height', maxHeight);
9922
9923 var api = $container.data('scrollable');
9924 if (api) {
9925 api.seekTo(api.getIndex(), 0);
9926 }
9927 };
9928
9929 options = $.extend(true,
9930 {
9931 scrollable:
9932 {
9933 circular: true,
9934 items: '.Panels'
9935 },
9936 navigator:
9937 {
9938 navi: '.Nav',
9939 naviItem: 'a',
9940 activeClass: 'current'
9941 },
9942 autoscroll:
9943 {
9944 interval: 3000
9945 }
9946
9947 }, options);
9948
9949 $container.css('overflow', 'hidden');
9950
9951 if (!options.scrollable.vertical) {
9952 $container.css('height', 'auto')
9953 .find('.Panels').css('width', '20000em')
9954 .find('.panel').css('float', (XenForo.isRTL() ? 'right' : 'left'));
9955 }
9956
9957 $(window).bind('load resize', resizeItems);
9958 $('.mainContent').bind('XenForoResize', resizeItems);
9959
9960 resizeItems();
9961
9962 $container.scrollable(options.scrollable).navigator(options.navigator);
9963
9964 if ($items.length > 1) {
9965 $container.autoscroll(options.autoscroll);
9966 }
9967
9968 $container.find('input.Disabler').on('click', function () {
9969 setInterval(resizeItems, XenForo.speed.fast);
9970 });
9971
9972 return $container.data('scrollable');
9973 };
9974
9975 // *********************************************************************
9976
9977 XenForo.DisplayIgnoredContent = function (e) {
9978 var i, j, styleSheet, rules, rule;
9979
9980 const isProfilePage = $('.profilePage').length;
9981 e.preventDefault();
9982
9983 let $hideContent = $('a.DisplayIgnoredContent')
9984 $hideContent.hide();
9985
9986 // remove the styling that hides quotes etc.
9987 $('#ignoredUserCss').empty().remove();
9988
9989 if (document.styleSheets) {
9990 for (i = 0; i < document.styleSheets.length; i++) {
9991 styleSheet = document.styleSheets[i];
9992 try {
9993 rules = (styleSheet.cssRules ? styleSheet.cssRules : styleSheet.rules);
9994 } catch (e) {
9995 rules = false;
9996 }
9997 if (!rules) {
9998 continue;
9999 }
10000 for (j = 0; j < rules.length; j++) {
10001 rule = rules[j];
10002
10003 if (rule.selectorText && rule.selectorText.toLowerCase() === '.ignored') {
10004 if (styleSheet.deleteRule) {
10005 styleSheet.deleteRule(j);
10006 } else {
10007 styleSheet.removeRule(j);
10008 }
10009 }
10010 }
10011 }
10012 }
10013
10014 $('.ignored').removeClass('ignored');
10015 if (isProfilePage && !$hideContent.closest('.pageNavLinkGroup').find('.PageNav').length) {
10016 $clone = $hideContent.closest('.pageNavLinkGroup').clone();
10017 $clone.find('.DisplayIgnoredContent').remove()
10018 if (!$clone.find('.linkGroup').children().length) $('.pageNavLinkGroup:visible').remove();
10019 }
10020 };
10021
10022 if ($('html').hasClass('Public')) {
10023 $(function () {
10024 $('body').delegate('a.DisplayIgnoredContent', 'click', XenForo.DisplayIgnoredContent);
10025
10026 if (window.location.hash) {
10027 var $jump = $(window.location.hash.replace(/[^\w_#-]/g, ''));
10028 if ($jump.hasClass('ignored')) {
10029 $jump.removeClass('ignored');
10030 $jump.get(0).scrollIntoView(true);
10031 }
10032 }
10033 });
10034 }
10035
10036 // *********************************************************************
10037
10038 XenForo.SpoilerBBCode = function ($spoiler) {
10039 $spoiler.click(function (e) {
10040 $spoiler.siblings(':first').css('fontSize', '25pt');
10041 return false;
10042 });
10043
10044 /*$spoiler.click(function(e)
10045 {
10046 $spoiler.html($spoiler.data('spoiler')).removeClass('Spoiler').addClass('bbCodeSpoiler');
10047 });*/
10048 };
10049
10050 // *********************************************************************
10051
10052 /**
10053 * Produces centered, square crops of thumbnails identified by data-thumb-selector within the $container.
10054 * Requires that CSS of this kind is in place:
10055 * .SquareThumb
10056 * {
10057 * position: relative; display: block; overflow: hidden;
10058 * width: {$thumbHeight}px; height: {$thumbHeight}px;
10059 * }
10060 * .SquareThumb img
10061 * {
10062 * position: relative; display: block;
10063 * }
10064 *
10065 * @param jQuery $container
10066 */
10067 XenForo.SquareThumbs = function ($container) {
10068 var thumbHeight = $container.data('thumb-height') || 44,
10069 thumbSelector = $container.data('thumb-selector') || 'a.SquareThumb';
10070
10071 console.info('XenForo.SquareThumbs: %o', $container);
10072
10073 var $imgs = $container.find(thumbSelector).addClass('SquareThumb').children('img');
10074
10075 var thumbProcessor = function () {
10076 var $thumb = $(this),
10077 w = $thumb.width(),
10078 h = $thumb.height();
10079
10080 if (!w || !h) {
10081 return;
10082 }
10083
10084 if (h > w) {
10085 $thumb.css('width', thumbHeight);
10086 $thumb.css('top', ($thumb.height() - thumbHeight) / 2 * -1);
10087 } else {
10088 $thumb.css('height', thumbHeight);
10089 $thumb.css('left', ($thumb.width() - thumbHeight) / 2 * -1);
10090 }
10091 };
10092
10093 $imgs.load(thumbProcessor);
10094 $imgs.each(thumbProcessor);
10095 };
10096
10097 // *********************************************************************
10098
10099 // Register overlay-loading controls
10100 // TODO: when we have a global click handler, change this to use rel="Overlay" instead of class="OverlayTrigger"
10101 XenForo.register(
10102 'a.OverlayTrigger, input.OverlayTrigger, button.OverlayTrigger, label.OverlayTrigger, a.username, a.avatar',
10103 'XenForo.OverlayTrigger'
10104 );
10105
10106 XenForo.register('.ToggleTrigger', 'XenForo.ToggleTrigger');
10107
10108
10109 // Register tooltip elements for desktop browsers
10110 XenForo.register('.Tooltip', 'XenForo.Tooltip');
10111
10112 //XenForo.register('a.StatusTooltip', 'XenForo.StatusTooltip');
10113 XenForo.register('.PreviewTooltip', 'XenForo.PreviewTooltip');
10114
10115 XenForo.register('a.LbTrigger', 'XenForo.LightBoxTrigger');
10116
10117 // Register click-proxy controls
10118 XenForo.register('.ClickProxy', 'XenForo.ClickProxy');
10119
10120 // Register popup menu controls
10121 XenForo.register('.Popup', 'XenForo.PopupMenu', 'XenForoActivatePopups');
10122
10123 // Register scrolly pagenav elements
10124 XenForo.register('.PageNav', 'XenForo.PageNav');
10125
10126 // Register tabs
10127 XenForo.register('.Tabs', 'XenForo.Tabs');
10128
10129 // Register square thumb cropper
10130 XenForo.register('.SquareThumbs', 'XenForo.SquareThumbs');
10131
10132 // Handle all xenForms
10133 XenForo.register('form.xenForm, .MultiSubmitFix', 'XenForo.MultiSubmitFix');
10134
10135 // Register check-all controls
10136 XenForo.register('input.CheckAll, a.CheckAll, label.CheckAll', 'XenForo.CheckAll');
10137
10138 // Register auto-checker controls
10139 XenForo.register('input.AutoChecker', 'XenForo.AutoChecker');
10140
10141 // Register toggle buttons
10142 XenForo.register('label.ToggleButton', 'XenForo.ToggleButton');
10143
10144 // Register auto inline uploader controls
10145 XenForo.register('form.AutoInlineUploader', 'XenForo.AutoInlineUploader');
10146
10147 // Register form auto validators
10148 XenForo.register('form.AutoValidator', 'XenForo.AutoValidator');
10149
10150 // Register auto time zone selector
10151 XenForo.register('select.AutoTimeZone', 'XenForo.AutoTimeZone');
10152
10153 // Register generic content loader
10154 XenForo.register('a.Loader, input.Loader', 'XenForo.Loader');
10155
10156 var supportsStep = 'step' in document.createElement('input');
10157
10158 // Register form controls
10159 XenForo.register('input, textarea', function (i) {
10160 var $this = $(this);
10161
10162 switch ($this.attr('type')) {
10163 case 'hidden':
10164 case 'submit':
10165 return;
10166 case 'checkbox':
10167 case 'radio':
10168 // Register auto submitters
10169 if ($this.hasClass('SubmitOnChange')) {
10170 XenForo.create('XenForo.SubmitOnChange', this);
10171 }
10172 return;
10173 }
10174
10175 // Spinbox / input[type=number]
10176 if ($this.attr('type') == 'number' && supportsStep) {
10177 // use the XenForo implementation instead, as browser implementations seem to be universally horrible
10178 this.type = 'text';
10179 $this.addClass('SpinBox number');
10180 }
10181 if ($this.hasClass('SpinBox')) {
10182 XenForo.create('XenForo.SpinBox', this);
10183 }
10184
10185 // Prompt / placeholder
10186 if ($this.hasClass('Prompt')) {
10187 console.error('input.Prompt[title] is now deprecated. Please replace any instances with input[placeholder] and remove the Prompt class.');
10188 $this.attr({ placeholder: $this.attr('title'), title: '' });
10189 }
10190 if ($this.attr('placeholder')) {
10191 XenForo.create('XenForo.Prompt', this);
10192 }
10193
10194 // LiveTitle
10195 if ($this.data('livetitletemplate')) {
10196 XenForo.create('XenForo.LiveTitle', this);
10197 }
10198
10199 // DatePicker
10200 if ($this.is(':date')) {
10201 XenForo.create('XenForo.DatePicker', this);
10202 }
10203
10204 // AutoComplete
10205 if ($this.hasClass('AutoComplete')) {
10206 XenForo.create('XenForo.AutoComplete', this);
10207 }
10208
10209 // UserTagger
10210 if ($this.hasClass('UserTagger')) {
10211 XenForo.create('XenForo.UserTagger', this);
10212 }
10213
10214 // AutoSelect
10215 if ($this.hasClass('AutoSelect')) {
10216 XenForo.create('XenForo.AutoSelect', this);
10217 }
10218
10219 // AutoValidator
10220 if (XenForo.isAutoValidatorField(this)) {
10221 XenForo.create('XenForo.AutoValidatorControl', this);
10222 }
10223
10224 if ($this.is('textarea.StatusEditor')) {
10225 XenForo.create('XenForo.StatusEditor', this);
10226 }
10227
10228 // Register Elastic textareas
10229 if ($this.is('textarea.Elastic')) {
10230 XenForo.create('XenForo.TextareaElastic', this);
10231 }
10232 });
10233
10234 // Register form previewer
10235 XenForo.register('form.Preview', 'XenForo.PreviewForm');
10236
10237 // Register field adder
10238 XenForo.register('a.FieldAdder, input.FieldAdder', 'XenForo.FieldAdder');
10239
10240 // Read status toggler
10241 XenForo.register('a.ReadToggle', 'XenForo.ReadToggle');
10242
10243 XenForo.StarContent = function ($link) {
10244 var callback = function (ajaxData) {
10245 if (!XenForo.hasResponseError(ajaxData)) {
10246 if ($link.hasClass('mainc')) {
10247 $link.removeClass('mainc');
10248 $link.attr('title', XenForo.phrases.addFave)
10249 } else {
10250 $link.addClass('mainc');
10251 $link.attr('title', XenForo.phrases.removeFave)
10252 }
10253 }
10254 };
10255
10256 $link.on('click', function (e) {
10257 e.preventDefault();
10258 XenForo.ajax($link.attr('href'), {}, callback);
10259 });
10260 };
10261
10262
10263 XenForo.MarketBalanceSwitcher = function ($button) {
10264 this.__construct($button);
10265 };
10266
10267 XenForo.MarketBalanceSwitcher.prototype =
10268 {
10269 __construct: function ($button) {
10270 this.$button = $button;
10271 this.showButtonFromCache();
10272 this.$button.on('click', $.context(this, 'switch'));
10273 },
10274
10275 switch: function (e) {
10276 e.preventDefault();
10277 e.stopPropagation();
10278 XenForo.showMarketBalanceEnabled = !XenForo.showMarketBalanceEnabled;
10279
10280 if (this.$button.hasClass('Enabled')) {
10281 localStorage.removeItem('market_show_balance_enabled');
10282 } else {
10283 localStorage.setItem('market_show_balance_enabled', '1');
10284 }
10285
10286 this.$button.toggleClass('Enabled');
10287 $('#NavigationAccountUsername').toggleClass('hidden');
10288 $('#NavigationAccountBalance').toggleClass('hidden');
10289 },
10290
10291 showButtonFromCache: function () {
10292 XenForo.showMarketBalanceEnabled = localStorage.hasOwnProperty('market_show_balance_enabled');
10293 if (XenForo.showMarketBalanceEnabled) {
10294 this.$button.addClass('Enabled');
10295 $('#NavigationAccountUsername').addClass('hidden');
10296 $('#NavigationAccountBalance').removeClass('hidden');
10297 }
10298 }
10299 };
10300
10301 XenForo.Modal = function ($content, options) {
10302 this.__construct($content, options)
10303 }
10304
10305 XenForo.Modal.prototype = {
10306 __construct: function ($content, options) {
10307 this.opened = false
10308 this.$modal = $('<div class="modal fade" />').append($content)
10309 if (!options) options = {}
10310
10311 if (options.top && options.top === 'center') {
10312 $content.css('align-self', 'center')
10313 } else if (options.top) {
10314 $content.css('top', options.top)
10315 }
10316
10317 if (options.close)
10318 this.$closeElems = this.$modal.find(options.close).on('click', $.proxy(this, 'hide'))
10319
10320 this.$trigger = options.trigger || $($content)
10321
10322 var self = this
10323 this.$modal.modal({
10324 show: false,
10325 zIndex: options.zIndex,
10326 severalModals: options.severalModals,
10327 preventClose: this.$trigger.data('unclosable') === true,
10328 unloadAlert: this.$trigger.data('alertonclose') === true
10329 }).bind({
10330 'show.bs.modal': function () {
10331 self.opened = true
10332 self.$trigger.trigger('onBeforeLoad')
10333 options.onBeforeLoad && options.onBeforeLoad()
10334 },
10335 'shown.bs.modal': function () {
10336 self.$trigger.trigger('onLoad')
10337 options.onLoad && options.onLoad()
10338 },
10339 'hide.bs.modal': function () {
10340 self.opened = false
10341 self.$trigger.trigger('onBeforeClose')
10342 options.onBeforeClose && options.onBeforeClose()
10343 },
10344 'hidden.bs.modal': function () {
10345 self.$trigger.trigger('onClose')
10346 options.onClose && options.onClose()
10347 }
10348 })
10349
10350 var overlayData = { // длÃвЂГ‚ВЏ обÃвЂГўвЂљВ¬ГѓВђГ‚°ÃвЂГўв‚¬ЕЎГѓВђГ‚½ÃÂ¾ÃÂ¹ ГѓвЂГ‚ÃÂ¾ÃÂ²ÃÂ¼ÃÂµÃвЂГ‚ВЃГѓвЂГўв‚¬ЕЎГѓВђГ‚¸ÃÂ¼ÃÂ¾ÃвЂГ‚ВЃГѓвЂГўв‚¬ЕЎГѓВђГ‚Вё
10351 load: function () { self.show(); return overlayData },
10352 close: function () { self.hide(); return overlayData },
10353 destroy: function () { self.destroy(); return overlayData },
10354 getOverlay: function () { return $content },
10355 getConf: function () { return options },
10356 getTrigger: function () { return self.$trigger },
10357 getClosers: function () { return self.$closeElems ? this.$closeElems : $() },
10358 isOpened: function () { return self.opened }
10359 }
10360
10361 this.$trigger.data('overlay', overlayData)
10362 },
10363
10364 show: function () { this.$modal.modal('show'); },
10365 load: function () { this.show() },
10366 destroy: function () { this.$modal.remove() },
10367 hide: function () { this.$modal.modal('hide') }
10368 }
10369
10370 XenForo.TimedMessage = function ($elem, timeout, callback) {
10371 this.__construct($elem, timeout, callback)
10372 }
10373
10374 XenForo.TimedMessage.prototype = {
10375 __construct($elem, timeout, callback) {
10376 this.closed = false
10377 this.$elem = $elem instanceof $ ? $elem : $($elem)
10378 this.callback = callback
10379
10380 $('body').append(this.$elem)
10381 this.$elem.css({
10382 top: 0,
10383 left: 0,
10384 position: 'fixed',
10385 display: 'block',
10386 zIndex: 15000,
10387 }).on('click', $.proxy(this, 'close'))
10388 setTimeout($.proxy(this, 'close'), timeout)
10389 },
10390
10391 close() {
10392 if (this.closed) return
10393 this.closed = true
10394 this.callback && this.callback()
10395
10396 this.$elem.animate({ opacity: 0 }, 500, function () {
10397 $(this).remove()
10398 })
10399 }
10400 }
10401
10402 /**
10403 * Public-only registrations
10404 */
10405 if ($('html').hasClass('Public')) {
10406
10407 XenForo.register('.StarContent', 'XenForo.StarContent');
10408
10409 // Register the login bar handle
10410 //XenForo.register('#loginBar', 'XenForo.LoginBar');
10411
10412 // Register the header search box
10413 XenForo.register('.QuickSearch', 'XenForo.QuickSearch');
10414
10415 // Register attribution links
10416 XenForo.register('a.AttributionLink', 'XenForo.AttributionLink');
10417
10418 // CAPTCHAS
10419 XenForo.register('#ReCaptcha', 'XenForo.ReCaptcha');
10420 XenForo.register('.NoCaptcha', 'XenForo.NoCaptcha');
10421 XenForo.register('#SolveMediaCaptcha', 'XenForo.SolveMediaCaptcha');
10422 XenForo.register('#KeyCaptcha', 'XenForo.KeyCaptcha');
10423 XenForo.register('#Captcha', 'XenForo.Captcha');
10424
10425 // Resize large BB code images
10426 XenForo.register('img.bbCodeImage', 'XenForo.BbCodeImage');
10427
10428 // Handle like/unlike links
10429 XenForo.register('a.LikeLink', 'XenForo.LikeLink');
10430
10431 // Register node description tooltips
10432 if (!XenForo.isTouchBrowser()) {
10433 //XenForo.register('h3.nodeTitle a', 'XenForo.NodeDescriptionTooltip');
10434 }
10435
10436 // Register visitor menu
10437 //XenForo.register('#AccountMenu', 'XenForo.AccountMenu');
10438 XenForo.register('#MarketBalanceSwitcher', 'XenForo.MarketBalanceSwitcher');
10439
10440 // Register follow / unfollow links
10441 XenForo.register('a.FollowLink', 'XenForo.FollowLink');
10442
10443 XenForo.register('li.PopupItemLink', 'XenForo.PopupItemLink');
10444
10445 // Register notices
10446 XenForo.register('.Notices', 'XenForo.Notices');
10447
10448 // Spoiler BB Code
10449 XenForo.register('button.Spoiler', 'XenForo.SpoilerBBCode');
10450 }
10451
10452 // Register control disablers last so they disable anything added by other behaviours
10453 XenForo.register('input:checkbox.Disabler, input:radio.Disabler', 'XenForo.Disabler');
10454
10455 // *********************************************************************
10456 var isScrolled = false;
10457 $(window).on('load', function () {
10458 if (isScrolled || !window.location.hash) {
10459 return;
10460 }
10461
10462 var hash = window.location.hash.replace(/[^a-zA-Z0-9_-]/g, ''),
10463 $match = hash ? $('#' + hash) : $();
10464
10465 if ($match.length) {
10466 $match.get(0).scrollIntoView(true);
10467 }
10468 });
10469
10470 /**
10471 * Use jQuery to initialize the system
10472 */
10473 $(function () {
10474 XenForo.init();
10475
10476 if (window.location.hash) {
10477 // do this after the document is ready as triggering it too early
10478 // causes the initial hash to trigger a scroll
10479 $(window).one('scroll', function (e) {
10480 isScrolled = true;
10481 });
10482 }
10483
10484 var scrollTrigger = 100; // px
10485 var backToTop = function () {
10486 var scrollTop = $(window).scrollTop();
10487 if (scrollTop > scrollTrigger) {
10488 $('.cd-top').addClass('cd-is-visible');
10489 } else {
10490 $('.cd-top').removeClass('cd-is-visible');
10491 }
10492 };
10493 $(window).on('load scroll', function () {
10494 backToTop();
10495 });
10496 $('.cd-top').on('click', function (e) {
10497 e.preventDefault();
10498 $('html,body').animate({
10499 scrollTop: 0
10500 }, 350);
10501 });
10502
10503 $('a[href^="#"]').on('click', function (e) {
10504 if (!$(this).hasClass('cd-top') && $(this.hash).offset()) {
10505 e.preventDefault();
10506 $('html,body').animate({ scrollTop: ($(this.hash).offset().top - 44) }, 500);
10507 }
10508 });
10509
10510 })
10511}
10512 (jQuery, this, document);
10513
10514+function ($) {
10515 'use strict';
10516
10517 // MODAL CLASS DEFINITION
10518 // ======================
10519
10520 var Modal = function (element, options) {
10521 this.options = options
10522 this.$body = $(document.body)
10523 this.$html = $('html')
10524 this.$element = $(element)
10525 this.$dialog = this.$element.find('.modal-dialog')
10526 this.$backdrop = null
10527 this.isShown = null
10528 this.originalBodyPad = null
10529 this.scrollbarWidth = 0
10530 this.ignoreBackdropClick = false
10531 this.fixedContent = ''; //'.navbar-fixed-top, .navbar-fixed-bottom, #navigation'
10532 this.zIndex = options.zIndex || 0
10533 this.zIndexOffset = 10001
10534 if (this.options.remote) {
10535 this.$element
10536 .find('.modal-content')
10537 .load(this.options.remote, $.proxy(function () {
10538 this.$element.trigger('loaded.bs.modal')
10539 }, this))
10540 }
10541
10542 this.$element
10543 .css('z-index', this.zIndexOffset + 1 + this.zIndex * 2)
10544 .attr('data-z-index', this.zIndex)
10545 }
10546
10547 Modal.VERSION = '3.4.1'
10548
10549 Modal.TRANSITION_DURATION = 200
10550 Modal.BACKDROP_TRANSITION_DURATION = 150
10551
10552 Modal.DEFAULTS = {
10553 backdrop: true,
10554 keyboard: true,
10555 show: true
10556 }
10557
10558 Modal.prototype.toggle = function (_relatedTarget) {
10559 return this.isShown ? this.hide() : this.show(_relatedTarget)
10560 }
10561
10562 Modal.prototype.show = function (_relatedTarget) {
10563 var $modal = $('.modal.in[data-z-index=' + this.zIndex + ']')
10564 if (this.options.severalModals) {
10565 this.showModal(_relatedTarget);
10566 var $fade = $('div.modal-backdrop.fade.in').last();
10567 $fade.css('z-index', parseInt($fade.css('z-index')) + 5);
10568 this.$element
10569 .css('z-index', parseInt($fade.css('z-index')) + 6)
10570 .attr('data-z-index', parseInt($fade.css('z-index')) + 6)
10571 return;
10572 }
10573 if ($modal.length) {
10574 $modal.data('bs.modal').hide(null, function () {
10575 this.showModal(_relatedTarget)
10576 }.bind(this))
10577 } else this.showModal(_relatedTarget)
10578 }
10579
10580 Modal.prototype.showModal = function (_relatedTarget) {
10581 var that = this
10582
10583 if (this.isShown) return
10584
10585 this.isShown = true
10586
10587 this.checkScrollbar()
10588 this.setScrollbar()
10589 this.$html.addClass('modal-open')
10590
10591 if (this.options.unloadAlert) {
10592 window.onbeforeunload = function () {
10593 return true;
10594 };
10595 }
10596
10597 if (!this.options.preventClose) this.escape()
10598 this.resize()
10599
10600 //this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
10601 this.$dialog.on('mousedown.dismiss.bs.modal', function () {
10602 that.$element.one('mouseup.dismiss.bs.modal', function (e) {
10603 if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true
10604 })
10605 })
10606
10607 if (!this.$element.parent().length) {
10608 this.$element.appendTo(that.$body) // don't move modals dom position
10609 }
10610
10611 var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
10612 this.$element.trigger(e)
10613
10614 this.backdrop(function () {
10615 var transition = that.$element.hasClass('fade')
10616
10617 that.$element
10618 .show()
10619 .scrollTop(0)
10620
10621 that.adjustDialog()
10622
10623 if (transition) {
10624 that.$element[0].offsetWidth // force reflow
10625 }
10626
10627 that.$element.addClass('in')
10628
10629 that.enforceFocus()
10630
10631 var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
10632 that.$element.find('.overlayScroll').addClass('scrollbar-macosx scrollbar-dynamic').scrollbar()
10633 that.$element.attr('tabindex', '-1').css('outline', 'none')
10634 transition ?
10635 setTimeout(function () {
10636 that.$element.trigger('focus').trigger(e)
10637 }, Modal.TRANSITION_DURATION) :
10638 that.$element.trigger('focus').trigger(e)
10639 })
10640 }
10641
10642 Modal.prototype.hide = function (e, cb) {
10643 if (e) e.preventDefault()
10644
10645 e = $.Event('hide.bs.modal')
10646
10647 this.$element.trigger(e)
10648
10649 if (!this.isShown || e.isDefaultPrevented()) return
10650
10651 this.isShown = false
10652
10653 this.escape()
10654 this.resize()
10655
10656 window.onbeforeunload = null;
10657
10658 $(document).off('focusin.bs.modal')
10659
10660 this.$element
10661 .removeClass('in')
10662 .off('click.dismiss.bs.modal')
10663 .off('mouseup.dismiss.bs.modal')
10664
10665 this.$dialog.off('mousedown.dismiss.bs.modal')
10666
10667 setTimeout(function () {
10668 this.hideModal(cb)
10669 }.bind(this), Modal.TRANSITION_DURATION)
10670 }
10671
10672 Modal.prototype.enforceFocus = function () {
10673 $(document)
10674 .off('focusin.bs.modal') // guard against infinite focus loop
10675 .on('focusin.bs.modal', $.proxy(function (e) {
10676 if (document !== e.target &&
10677 this.$element[0] !== e.target &&
10678 !this.$element.has(e.target).length) {
10679 this.$element.trigger('focus')
10680 }
10681 }, this))
10682 }
10683
10684 Modal.prototype.escape = function () {
10685 if (this.isShown && this.options.keyboard) {
10686 this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) {
10687 e.which == 27 && this.hide()
10688 }, this))
10689 } else if (!this.isShown) {
10690 this.$element.off('keydown.dismiss.bs.modal')
10691 }
10692 }
10693
10694 Modal.prototype.resize = function () {
10695 if (this.isShown) {
10696 $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this))
10697 } else {
10698 $(window).off('resize.bs.modal')
10699 }
10700 }
10701
10702 Modal.prototype.hideModal = function (cb) {
10703 var that = this
10704 this.$element.hide()
10705 this.backdrop(function () {
10706 if (!$('.modal.in:visible').length)
10707 that.$html.removeClass('modal-open')
10708 that.resetAdjustments()
10709 that.resetScrollbar()
10710 that.$element.trigger('hidden.bs.modal')
10711 let $modal = $('.modal.in:visible');
10712 if ($modal.length) {
10713 $modal.attr('tabindex', '-1')
10714 $modal.trigger('focus')
10715 }
10716 $('.modal.in:hidden').removeClass('in')
10717 if (cb) cb()
10718 })
10719 }
10720
10721 Modal.prototype.removeBackdrop = function () {
10722 this.$backdrop && this.$backdrop.remove()
10723 this.$backdrop = null
10724 }
10725
10726 Modal.prototype.backdrop = function (callback) {
10727 var that = this
10728 var animate = this.$element.hasClass('fade') ? 'fade' : ''
10729
10730 if (this.isShown && this.options.backdrop) {
10731 var doAnimate = animate
10732
10733 this.$backdrop = $(document.createElement('div'))
10734 .addClass('modal-backdrop ' + animate)
10735 .appendTo(this.$body)
10736 .css('z-index', this.zIndexOffset + this.zIndex * 2)
10737
10738 if (!this.options.preventClose) {
10739 this.$element.on('mousedown.dismiss.bs.modal touchstart.dismiss.bs.modal', $.proxy(function (e) {
10740 if (this.ignoreBackdropClick) {
10741 this.ignoreBackdropClick = false
10742 return
10743 }
10744 if (e.target !== e.currentTarget) return
10745
10746
10747
10748 if (this.$element[0].scrollHeight > document.documentElement.clientHeight && e.clientX > this.$backdrop.width() - this.scrollbarWidth)
10749 return
10750 this.options.backdrop == 'static'
10751 ? this.$element[0].focus()
10752 : this.hide()
10753 }, this))
10754 }
10755 if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
10756
10757 this.$backdrop.addClass('in')
10758
10759 if (!callback) return
10760 if (!doAnimate) return callback()
10761
10762 setTimeout(function () {
10763 callback()
10764 }.bind(this), Modal.BACKDROP_TRANSITION_DURATION)
10765
10766 } else if (!this.isShown && this.$backdrop) {
10767 this.$backdrop.removeClass('in')
10768
10769 var callbackRemove = function () {
10770 that.removeBackdrop()
10771 callback && callback()
10772 }
10773
10774 if (this.$element.hasClass('fade'))
10775 setTimeout(function () {
10776 callbackRemove()
10777 }.bind(this), Modal.BACKDROP_TRANSITION_DURATION)
10778
10779 } else if (callback) {
10780 callback()
10781 }
10782 }
10783
10784 // these following methods are used to handle overflowing modals
10785
10786 Modal.prototype.handleUpdate = function () {
10787 this.adjustDialog()
10788 }
10789
10790 Modal.prototype.adjustDialog = function () {
10791 var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight
10792
10793 this.$element.css({
10794 paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '',
10795 paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : ''
10796 })
10797 }
10798
10799 Modal.prototype.resetAdjustments = function () {
10800 this.$element.css({
10801 paddingLeft: '',
10802 paddingRight: ''
10803 })
10804 }
10805
10806 Modal.prototype.checkScrollbar = function () {
10807 var fullWindowWidth = window.innerWidth
10808 if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8
10809 var documentElementRect = document.documentElement.getBoundingClientRect()
10810 fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left)
10811 }
10812 this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth
10813 this.scrollbarWidth = this.measureScrollbar()
10814 }
10815
10816 Modal.prototype.setScrollbar = function () {
10817 if ($('.modal.in:visible').length) return;
10818
10819 var bodyPad = parseInt((this.$html.css('padding-right') || 0), 10)
10820 this.originalBodyPad = document.body.style.paddingRight || ''
10821 var scrollbarWidth = this.scrollbarWidth
10822 if (this.bodyIsOverflowing) {
10823 this.$html.css('padding-right', bodyPad + scrollbarWidth)
10824 $(this.fixedContent).each(function (index, element) {
10825 var actualPadding = element.style.paddingRight
10826 var calculatedPadding = $(element).css('padding-right')
10827 $(element)
10828 .data('padding-right', actualPadding)
10829 //.css('padding-right', parseFloat(calculatedPadding) + scrollbarWidth + 'px')
10830 })
10831 if ($('#ChatboxStartIcon').length)
10832 $('#ChatboxStartIcon')
10833 .css('transition', 'none')
10834 .css('right', (15 + scrollbarWidth) + 'px')
10835 if ($('#completeChatbox').length)
10836 $('#completeChatbox')
10837 .data('right', $('#completeChatbox').css('right'))
10838 .css('right', (parseInt($('#completeChatbox').css('right')) + scrollbarWidth) + 'px')
10839 }
10840 }
10841
10842 Modal.prototype.resetScrollbar = function () {
10843 if ($('.modal.in:visible').length) return;
10844
10845 this.$html.css('padding-right', this.originalBodyPad)
10846 $(this.fixedContent).each(function (index, element) {
10847 var padding = $(element).data('padding-right')
10848 $(element).removeData('padding-right')
10849 element.style.paddingRight = padding ? padding : ''
10850 })
10851 if ($('#ChatboxStartIcon').length) {
10852 $('#ChatboxStartIcon').css('right', '15px')
10853 setTimeout(function () {
10854 $('#ChatboxStartIcon').css('transition', '.2s')
10855 }, 250)
10856 }
10857 if ($('#completeChatbox').length)
10858 $('#completeChatbox').css('right', $('#completeChatbox').data('right'))
10859 }
10860
10861 Modal.prototype.measureScrollbar = function () {
10862 return window.innerWidth - document.documentElement.getBoundingClientRect().width
10863 }
10864
10865
10866 // MODAL PLUGIN DEFINITION
10867 // =======================
10868
10869 function Plugin(option, _relatedTarget) {
10870 return this.each(function () {
10871 var $this = $(this)
10872 var data = $this.data('bs.modal')
10873
10874 var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
10875 if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
10876 if (typeof option == 'string') data[option](_relatedTarget)
10877 else if (options.show) data.show(_relatedTarget)
10878 })
10879 }
10880
10881 var old = $.fn.modal
10882
10883 $.fn.modal = Plugin
10884 $.fn.modal.Constructor = Modal
10885
10886
10887 // MODAL NO CONFLICT
10888 // =================
10889
10890 $.fn.modal.noConflict = function () {
10891 $.fn.modal = old
10892 return this
10893 }
10894
10895
10896 // MODAL DATA-API
10897 // ==============
10898
10899 $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
10900 var $this = $(this)
10901 var href = $this.attr('href')
10902 var target = $this.attr('data-target') ||
10903 (href && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
10904
10905 var $target = $(document).find(target)
10906 var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
10907
10908 if ($this.is('a')) e.preventDefault()
10909
10910 $target.one('show.bs.modal', function (showEvent) {
10911 if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown
10912 $target.one('hidden.bs.modal', function () {
10913 $this.is(':visible') && $this.trigger('focus')
10914 })
10915 })
10916 Plugin.call($target, option, this)
10917 })
10918
10919 XenForo.SmiliesPicker = function ($element) {
10920 this.__construct($element);
10921 };
10922
10923 XenForo.SmiliesPicker.prototype =
10924 {
10925 __construct: function ($element) {
10926 console.log("[Smilies] Loading smilies for", $element)
10927 this.$element = $element;
10928 if (this.$element.get(0)._tippy !== undefined) {
10929 this.$element.get(0)._tippy.destroy()
10930 }
10931 this.$element.on('mousedown', function (e) {
10932 e.preventDefault()
10933 })
10934 this.$element.off('click')
10935 this.$smilies = undefined;
10936 this.localStorageKey = XenForo.smiliesCacheKey;
10937 this.smiliesLoaded = false;
10938 this.recentlyUsedLimit = 14;
10939 let ownerDocument = document.implementation.createHTMLDocument('virtual'); // Used to edit img tags without loading them
10940 if (supports_html5_storage()) {
10941 let temp = localStorage.getItem(this.localStorageKey);
10942 if (temp && temp.startsWith("<")) {
10943 this.cleanSmiliesCache();
10944 this.smiliesBox = $(temp, ownerDocument);
10945 this.smiliesBox.find('img').map(function () {
10946 $(this).attr("data-src", $(this).attr("src"));
10947 $(this).removeAttr('src');
10948 $(this).css({ 'display': 'none' });
10949 })
10950 this.createSmiliesBox();
10951 this.smiliesLoaded = true;
10952 }
10953 }
10954
10955 if (!this.smiliesLoaded) {
10956 XenForo.ajax(
10957 XenForo._editorSmiliesUrl,
10958 {},
10959 function (ajaxData) {
10960 if (XenForo.hasResponseError(ajaxData)) {
10961 return;
10962 }
10963
10964 if (ajaxData.templateHtml) {
10965 new XenForo.ExtLoader(ajaxData, function () {
10966 let recentlyUsedSmilies = this.getRecentlyUsedSmiliesFromCache();
10967 this.cleanSmiliesCache();
10968 this.smiliesBox = $(ajaxData.templateHtml, ownerDocument);
10969 this.smiliesBox.find('img').map(function () {
10970 $(this).attr("data-src", $(this).attr("src"));
10971 $(this).removeAttr('src');
10972 $(this).css({ 'display': 'none' });
10973 })
10974
10975 if (recentlyUsedSmilies) {
10976 this.smiliesBox.find('.RecentlyUsedSmilies').html(recentlyUsedSmilies).removeClass('hidden');
10977 }
10978 if (supports_html5_storage()) {
10979 try {
10980 localStorage.setItem(this.localStorageKey, this.smiliesBox[0].outerHTML);
10981 } catch (e) {
10982 this.clearLocalStorage(256);
10983 localStorage.setItem(this.localStorageKey, this.smiliesBox[0].outerHTML);
10984 }
10985 }
10986 this.createSmiliesBox()
10987 }.bind(this));
10988 }
10989 }.bind(this)
10990 );
10991 }
10992 },
10993
10994 cleanSmiliesCache: function () {
10995 let values = Object.keys(localStorage)
10996 .filter((key) => key !== this.localStorageKey && !isNaN(+new Date(parseInt(key) * 1000)) && localStorage.getItem(key).indexOf('<li class="Smilie') !== -1);
10997
10998 values.forEach(function (item, i, arr) {
10999 localStorage.removeItem(item);
11000 });
11001 },
11002
11003 locateSmiliesElement: function () {
11004 if (this.$element.closest('.input').length) {
11005 return this.$element.closest('.input').find('#smiliesBox');
11006 } else if (this.$element.closest('.commentSubmit').length) {
11007 if (!this.$element.closest('.commentSubmit').find('.redactor_smilies').length) {
11008 this.$element.closest('.commentSubmit').prepend($('<div class="redactor_smilies" />'));
11009 }
11010 return this.$element.closest('.commentSubmit').find('.redactor_smilies');
11011 } else if (this.$element.closest('.redactor_box').length) {
11012 if (!this.$element.closest('.redactor_box').parent().find('.redactor_smilies').length) {
11013 this.$element.closest('.redactor_box').parent().prepend($('<div class="redactor_smilies" />'));
11014 }
11015 return this.$element.closest('.redactor_box').parent().find('.redactor_smilies');
11016 } else if (this.$element.closest('form').length) {
11017 if (!this.$element.closest('form').find('.redactor_smilies').length) {
11018 this.$element.closest('form').prepend($('<div class="redactor_smilies" />'));
11019 }
11020 return this.$element.closest('form').find('.redactor_smilies');
11021 }
11022 },
11023
11024 locateInputElement: function () {
11025 if (this.$element.parent().find('input[type=text]').length) {
11026 return this.$element.parent().find('input[type=text]');
11027 } else if (this.$element.closest('.commentSubmit').length) {
11028 return this.$element.closest('.commentSubmit');
11029 } else if (this.$element.closest('.redactor_box').length) {
11030 return this.$element.closest('.redactor_box').parent();
11031 } else if (this.$element.closest('form').length) {
11032 return this.$element.closest('form');
11033 }
11034 },
11035
11036 setContent: function (content) {
11037 if (XenForo.isTouchBrowser()) {
11038 this.$smilies.html(content.children()).xfActivate()
11039 } else {
11040 this.$element.get(0)._tippy.setContent(content)
11041 }
11042 this.bind();
11043 },
11044
11045 getRecentlyUsedSmiliesFromCache: function () {
11046 let values = Object.keys(localStorage)
11047 .filter((key) => key !== this.localStorageKey && !isNaN(+new Date(parseInt(key) * 1000)) && localStorage.getItem(key).indexOf('<li class="Smilie') !== -1);
11048
11049 let returnValue = null;
11050 values.forEach(function (item, i, arr) {
11051 returnValue = $(localStorage.getItem(item)).find('.RecentlyUsedSmilies').html();
11052 });
11053
11054 return returnValue;
11055 },
11056
11057 addSmilieToRecentlyUsed: function ($smilie) {
11058 let selector = '.' + $smilie.attr('class').replace(' ', '.');
11059 let $cachedSmilie = this.$smilies.find('.RecentlyUsedSmilies').find(selector);
11060 if ($cachedSmilie.length) {
11061 $cachedSmilie.remove();
11062 }
11063 let $clone = $smilie.clone();
11064
11065 $clone.xfInsert('prependTo', this.$smilies.find('.RecentlyUsedSmilies').find('ul'), 'show', 0, function () {
11066 $clone.css('display', '');
11067 this.$smilies.find('.RecentlyUsedSmilies').removeClass('hidden');
11068 this.$smilies.find('.RecentlyUsedSmilies .Smilie').off('click').slice(this.recentlyUsedLimit).remove();
11069 if (supports_html5_storage()) {
11070 this.$save = this.$smilies.clone()
11071 this.$save.find('img').map(function () {
11072 $(this).attr("data-src", $(this).attr("src"));
11073 $(this).removeAttr('src');
11074 $(this).css({ 'display': 'none' });
11075 })
11076 try {
11077 localStorage.setItem(this.localStorageKey, this.$save.html());
11078 } catch (e) {
11079 this.clearLocalStorage(256);
11080 localStorage.setItem(this.localStorageKey, this.$save.html());
11081 }
11082 }
11083 }.bind(this));
11084 },
11085
11086 appendToInput: function ($smilie) {
11087 let $input = this.locateInputElement()
11088 if ($input.is("input")) {
11089 $input.val($input.val() + $smilie.find('img').attr('alt')).focus();
11090 } else {
11091 let $ed = XenForo.getEditorInForm($input);
11092 $ed.execCommand('inserthtml', $smilie.html().trim());
11093 $ed.focus();
11094 }
11095 },
11096
11097 createSmiliesBox: function () {
11098 if (XenForo.isTouchBrowser()) {
11099 this.loadMobile();
11100 } else {
11101 this.loadDesktop();
11102 }
11103 },
11104
11105 updateSmiliesKits: function ($el) {
11106 $el.addClass('updated-smilies')
11107 const $nav = $('<div class="smilies-nav"><div class="smilies-nav-container"></div></div>')
11108 $nav.find('.smilies-nav-container')
11109 .append($el.find('.navigation').children().clone())
11110 $el.find('.navigation').css('display', 'none');
11111 $el.append($nav)
11112 },
11113
11114 loadDesktop: function () {
11115 this.smiliesBox.find('img').map(function () {
11116 $(this).css({ 'display': 'none' });
11117 })
11118
11119 let content = this.smiliesBox[0].outerHTML
11120 let d = document.createElement('div');
11121 let contentSelector = 'chatboxSmilies';
11122 $(d).attr('id', contentSelector).addClass('simpleRedactor--smiliesBox').hide();
11123 $(d).html(content).show();
11124 this.updateSmiliesKits($(d))
11125 this.$smilies = $(d);
11126 let isFirstShown = true;
11127 let $content = $('<div>').append(this.$smilies.clone());
11128
11129 this._tippy = tippy(this.$element.get(0), {
11130 content: $content.html(),
11131 theme: 'popup',
11132 interactive: true,
11133 arrow: true,
11134 animation: 'shift-toward',
11135 hideOnClick: false,
11136 zIndex: this.$element.closest('.xenOverlay').length || this.$element.closest('.StackAlerts').length ? 11111 : 9999,
11137 onHidden: function () {
11138 this.$smilies.find('ul.smilieContainer').get(0).scrollTop = 0;
11139 }.bind(this),
11140 onShown: function (element) {
11141 this.$smilies = $(element.popper).find('#chatboxSmilies');
11142 // If element position is over real page size
11143 var picker = $('.tippy-popper')[0].getBoundingClientRect()
11144 if (window.innerWidth < (picker.x + picker.width)) {
11145 var amount = picker.x + picker.width - window.innerWidth + 20
11146 $('.tippy-popper').css('margin-left', '-' + amount + 'px').css('margin-right', amount + 'px')
11147 } else {
11148 if (window.innerWidth > (picker.x + picker.width + parseInt($('.tippy-popper').css('margin-right'), 10))) {
11149 $('.tippy-popper').css('margin-left', '0px').css('margin-right', '0px')
11150 }
11151 }
11152
11153 if (!isFirstShown) return;
11154 // Lazy loading
11155 $content.find('img').map(function () {
11156 $(this).attr('src', $(this).data("src"));
11157 $(this).css({ 'display': '' });
11158 })
11159 this.$element.get(0)._tippy.setContent($content.html());
11160 this.$smilies = $(element.popper).find('#chatboxSmilies');
11161 $('.smilies-nav-container')
11162 .addClass('scrollbar-macosx scrollbar-dynamic').scrollbar()
11163 .siblings('.scroll-x').find('.scroll-bar').css('left', 0);
11164 this.bind()
11165 isFirstShown = false;
11166 }.bind(this)
11167 });
11168
11169 let $input = this.locateInputElement()
11170 if (!$input.is("input")) {
11171 $input = XenForo.getEditorInForm($input).$editor;
11172 if (!$input) {
11173 return
11174 }
11175 }
11176 $input.on('keydown', function (e) {
11177 if (e.keyCode === 9 && !!this._tippy) {
11178 e.preventDefault();
11179 if (this._tippy.state.isShown) {
11180 this._tippy.hide()
11181 } else {
11182 this._tippy.show()
11183 }
11184 }
11185 }.bind(this))
11186 },
11187
11188 slideSmilies: function () {
11189 this.$smilies.slideToggle();
11190
11191 const scrollElement = $('body,html')
11192 const scrollTop = $(".smilieContainer").offset().top - 200
11193 if (Math.abs(scrollElement.scrollTop() - scrollTop) > 100) {
11194 scrollElement.animate({ scrollTop }, { duration: 450, queue: false });
11195 }
11196 },
11197
11198 loadMobile: function () {
11199 this.$element.off('click').click(function (e) {
11200 e.preventDefault();
11201 e.stopImmediatePropagation();
11202 this.$smilies = this.locateSmiliesElement();
11203
11204 if (!this.$smilies.length) {
11205 return;
11206 }
11207
11208 if (this.$smilies.children().length) {
11209 this.slideSmilies();
11210 return;
11211 }
11212
11213 this.smiliesBox.find('img').map(function () {
11214 $(this).attr('src', $(this).data("src"));
11215 $(this).css({ 'display': '' });
11216 })
11217
11218 this.$smilies.html(this.smiliesBox[0].outerHTML)
11219 this.$smilies.hide();
11220 this.bind();
11221 this.slideSmilies();
11222 }.bind(this));
11223 },
11224
11225 bind: function () {
11226 this.$smilies.xfActivate();
11227 this.$smilies.off('click').on('click', '.Smilie', function (e) {
11228 let target = e.target.closest('li') || e.target;
11229 this.appendToInput($(target));
11230
11231 if (!$(target).closest('.RecentlyUsedSmilies').length) {
11232 this.addSmilieToRecentlyUsed($(target));
11233 }
11234
11235 const acResults = $('.autoCompleteListSmilies:visible')
11236 if (acResults && acResults.length) {
11237 acResults.data('XenForo.AutoSmiliesCompleteResults').hideResults()
11238 }
11239 return true;
11240 }.bind(this));
11241 const kitsPositions = []
11242 this.$smilies.find('.NavigationSmilie').each(function () {
11243 const elem = $($(this).data("id").replace("Id", ""))
11244 const parent = $("ul.smilieContainer")
11245 kitsPositions.push([elem.attr('id'), elem.offset().top - parent.offset().top])
11246 const self = $(this)
11247 self.off('click').on('click', function (e) {
11248 $('.smilies-nav-container li.active-kit').removeClass('active-kit')
11249 self.addClass('active-kit')
11250 parent.scrollTop(parent.scrollTop() + (elem.offset().top - parent.offset().top));
11251 });
11252 });
11253
11254 if (!XenForo.isTouchBrowser()) {
11255 const navContainer = $(".smilies-nav-container")[1]
11256 navContainer.addEventListener("wheel", e => {
11257 e.preventDefault();
11258 navContainer.scrollLeft += (Math.abs(e.deltaY) > Math.abs(e.deltaX) ? e.deltaY : e.deltaX) * 0.5;
11259 });
11260
11261 let prev_active_kit = 'smilieCategory--0'
11262 this.$smilies.find('.smilieContainer').off('scroll').on('scroll', function (e) {
11263 const scroll_pos = $(this).scrollTop()
11264 kitsPositions.some((item, index) => {
11265 if (scroll_pos < item[1]) {
11266 const active_kit = kitsPositions[index === 0 ? 0 : index - 1][0]
11267 if (active_kit !== prev_active_kit) {
11268 prev_active_kit = active_kit
11269
11270 const parent = $('.smilies-nav-container')
11271 const elem = $('.smilies-nav-container li[data-id="#' + active_kit.replace('--', 'Id--') + '"]')
11272 $('.smilies-nav-container li.active-kit').removeClass('active-kit')
11273 elem.addClass('active-kit')
11274 parent.animate({
11275 scrollLeft: parent.scrollLeft() + (elem.offset().left - parent.offset().left)
11276 }, { duration: 200, queue: false });
11277 }
11278 return true
11279 }
11280 })
11281 })
11282 }
11283 },
11284
11285 clearLocalStorage: function (counter) {
11286 if (this.getFreeLocalStorageSize(counter) <= 256) { // kB
11287 let values = Object.keys(localStorage)
11288 .filter((key) => key.startsWith('market_search_bar'));
11289 for (let i = 0; i < values.length; i++) {
11290 localStorage.removeItem(values[i])
11291 if (this.getFreeLocalStorageSize(counter) >= 512) {
11292 return
11293 }
11294 }
11295 }
11296 },
11297
11298 getFreeLocalStorageSize: function (counter) {
11299 localStorage.removeItem('XenForo');
11300 if (localStorage && !localStorage.getItem('XenForo')) {
11301 let i = 0;
11302 try {
11303 for (i = counter; i <= 10000; i += counter) {
11304 localStorage.setItem('XenForo', new Array((i * 1024) + 1).join('a'));
11305 }
11306 } catch (e) {
11307 localStorage.removeItem('XenForo');
11308 return i - counter;
11309 }
11310 }
11311 },
11312 };
11313
11314 XenForo.DisableButton = function ($el) {
11315 var $spinner = $('<i class="fa fa-spinner fa-spin"></i>').hide().appendTo($el)
11316 var loading = false
11317 $el.on('click', function (e) {
11318 if (loading) e.preventDefault()
11319 })
11320 $el.on('BeforeOverlayTrigger', function (e) {
11321 if (loading) e.preventDefault()
11322 loading = true
11323 $spinner.show()
11324 $el.addClass('disabled')
11325 })
11326 $(document).on('OverlayOpening', function () {
11327 if (!loading) return;
11328 loading = false
11329 $spinner.hide()
11330 $el.removeClass('disabled')
11331 })
11332 }
11333 XenForo.register('.DisableButton', 'XenForo.DisableButton')
11334
11335 XenForo.register('.smiliePicker', 'XenForo.SmiliesPicker');
11336 XenForo.register('.SmiliesTooltip', 'XenForo.SmiliesPicker');
11337 XenForo.register('.redactor_btn_smilies', 'XenForo.SmiliesPicker');
11338 XenForo.register('.redactor_btn_container_smilies', 'XenForo.SmiliesPicker');
11339 XenForo.register('span.ImNotification--Smiliepicker', 'XenForo.SmiliesPicker');
11340
11341 // *********************************************************************
11342
11343 XenForo.ReportForm = function ($element) {
11344 $element.find('input[type="radio"]').labelauty();
11345 $element.find('.reportReasons > label').on("click", function (e) {
11346 e.stopPropagation();
11347 e.stopImmediatePropagation();
11348 e.preventDefault();
11349 $element.find('._insertHereReason').val($(this).find('.labelauty-checked').text());
11350 $element.find('input[name="is_common_reason"]').val("1");
11351 $element.trigger('submit');
11352 });
11353 };
11354
11355 XenForo.register('._reportForm', 'XenForo.ReportForm');
11356
11357 // *********************************************************************
11358
11359 XenForo.SuggestThreads = function ($selector) {
11360 XenForo.scriptLoader.loadCss(['discussion_list'], "css.php?css=__sentinel__&style=9&_v=" + XenForo._jsVersion, function () {
11361 this.__construct($selector);
11362 }.bind(this), function () { });
11363 };
11364
11365 XenForo.SuggestThreads.prototype = {
11366 __construct: function ($select) {
11367 if (!$select.hasClass('SuggestThreads')) return;
11368 this.$input = $select;
11369 this.$element = $select.closest('.titleUnit');
11370
11371 let $fieldset = $select.closest('fieldset');
11372 $fieldset.find('dl').each((_, element) => {
11373 if (!$(element).hasClass('fullWidth'))
11374 $fieldset.before($(element));
11375 })
11376 $fieldset.parent().xfActivate();
11377
11378 let timer;
11379 $('<div class="discussionList" style="padding: 0px !important;"><div class="latestThreads _insertLoadedContent"/></div>').appendTo(this.$element);
11380 this.$input.on('keyup', function () {
11381 clearTimeout(timer);
11382 timer = setTimeout(() => { this.hitEdit() }, 400);
11383 }.bind(this))
11384 },
11385
11386 hitEdit: function () {
11387 let value = this.$input.val().trim();
11388 let counter = 0;
11389 if (!value) return $('._insertLoadedContent').css('padding-top', '0px').html('');
11390 XenForo.ajax(window.location.href.substring(0, window.location.href.lastIndexOf('/') + 1), { order: 'last_post_date', direction: 'desc', period: '', state: 'active', 'title': value }, function (ajaxData) {
11391 var $data = $(ajaxData.templateHtml).find('._insertLoadedContent');
11392 $('._insertLoadedContent').css('padding-top', '0px').html('');
11393 if (!$data.length) return;
11394 $('._insertLoadedContent').replaceWith($data).xfActivate();
11395 $(`<p class="textHeading">${XenForo.phrases.check_similar_threads}</p>`).prependTo($('._insertLoadedContent'));
11396 $('._insertLoadedContent').css('padding-top', '20px').find('.discussionListItem').each(function () {
11397 counter++;
11398 if (counter > 5) $(this).remove();
11399 })
11400 })
11401 }
11402 }
11403
11404 XenForo.register('#ctrl_title_thread_create', 'XenForo.SuggestThreads');
11405
11406 // *********************************************************************
11407
11408 XenForo.PasswordInput = function ($element) {
11409 this.$element = $element;
11410 if (!this.$element.is(':visible')) return;
11411 this.$parent = $element.wrap('<div class="inputRelative"/>').parent();
11412 this.$button = $('<i class="inputRelativeIcon fas fa-eye"/>').appendTo(this.$parent);
11413 this.$button.on('click', function () {
11414 if (this.$element.attr('type') === 'password') {
11415 this.$button.removeClass('fa-eye').addClass('fa-eye-slash');
11416 this.$element.attr('type', 'text');
11417 } else {
11418 this.$button.removeClass('fa-eye-slash').addClass('fa-eye');
11419 this.$element.attr('type', 'password');
11420 }
11421 }.bind(this))
11422 };
11423
11424 XenForo.register('input[type=password]', 'XenForo.PasswordInput');
11425
11426 // *********************************************************************
11427
11428 XenForo.EnhancedAutoComplete = function ($selector) {
11429 $selector.prop("disabled", true);
11430 XenForo.scriptLoader.loadCss(['select2'], "css.php?css=__sentinel__&style=9&_v=" + XenForo._jsVersion, function () {
11431 this.__construct($selector);
11432 }.bind(this), function () { });
11433 };
11434
11435 XenForo.EnhancedAutoComplete.prototype = {
11436 __construct: function ($element) {
11437 this.$element = $element;
11438 this.$element.prop("disabled", false);
11439 this.uniqueName = "enhancedAutoComplete" + Math.floor(Math.random() * new Date());
11440 this.$parent = this.$element.wrap(`<div class="${this.uniqueName}"/>`).parent();
11441 this.$select = $(`<select multiple="multiple" class="textCtrl" data-placeholder="${this.$element.attr('placeholder') || ''}" aria-hidden="true"></select>`).appendTo(this.$parent);
11442 this.height = this.$element.height();
11443 this.$select.select2({
11444 escapeMarkup: function (val) {
11445 return DOMPurify.sanitize(val);
11446 },
11447 width: this.$element.width() + 20 + "px",
11448 maximumSelectionLength: this.$element.hasClass('AcSingle') ? 1 : 0,
11449 dropdownParent: $('<div/>'),
11450 sorter: function (sort) {
11451 this.bind()
11452 return sort;
11453 }.bind(this)
11454 });
11455 $(`<style>.${this.uniqueName} li.select2-selection__choice {
11456 height:${this.height - 10}px !important;
11457 line-height:${this.height - 10}px !important;
11458 }
11459 .${this.uniqueName} img.smallCompleteAvatar {
11460 width: ${this.height - 14}px;
11461 height: ${this.height - 14}px;
11462 }
11463 </style>`).appendTo(this.$parent)
11464 this.$element.css('display', 'none');
11465 this.$element.attr("class").split(/\s+/).map((val) => {
11466 this.$parent.find('.select2-selection__rendered').addClass(val)
11467 if (val !== "textCtrl") this.$parent.find('.select2').addClass(val)
11468 })
11469 this.$parent.find('.select2-selection__rendered').attr('style', 'height: inherit !important;')
11470 this.acResults = new XenForo.AutoCompleteResults({
11471 onInsert: $.context(this, 'insertAutoComplete')
11472 });
11473 this.$parent.find('.select2-search__field').addClass('AutoComplete').xfActivate();
11474 if (this.$element.val()) this.initializeOld(this.$element.val().split(","));
11475 this.bind();
11476 this.$select.on('change', function () {
11477 let elements = [];
11478 let data = this.$select.select2('data');
11479 for (const el in data) {
11480 if (data[el].selected) elements.push(data[el].id);
11481 }
11482 this.$element.val(elements.join(","));
11483 this.$element.trigger('change');
11484 setTimeout(() => { this.bind() })
11485 }.bind(this));
11486 if (this.$parent.find('.select2-search__field').css('width') && !this.$parent.find('.select2-search__field').attr('width')) {
11487 this.$parent.find('.select2-search__field').attr('width', 'set');
11488 let padding = parseInt(window.getComputedStyle(this.$parent.find('.select2-selection__rendered').get(0), null).getPropertyValue('padding-left'));
11489 if (this.$parent.find('.select2-search__field').width() - padding > 0) {
11490 this.$parent.find('.select2-search__field').css('width', this.$parent.find('.select2-search__field').width() - padding + 'px');
11491 }
11492 }
11493 if (this.$element.val() && this.$element.val().split(",").length === 1) {
11494 XenForo.ajax(XenForo.AutoComplete.getDefaultUrl(), { q: this.$element.val() }, function (ajaxData) {
11495 if (ajaxData.results !== []) {
11496 this.$element.data('result', ajaxData);
11497 this.initializeOld(this.$element.val().split(","));
11498 }
11499 }.bind(this))
11500 }
11501 },
11502
11503 findMatch: function (match) {
11504 let resultKeys = Object.keys((this.$element.data('result') || {}).results || {});
11505 for (let i = 0; i < resultKeys.length; i++) {
11506 if (resultKeys[i].toLowerCase() === match.toLowerCase()) {
11507 return resultKeys[i]
11508 }
11509 }
11510 return false;
11511 },
11512
11513 formatAvatar: function (result) {
11514 let $html = $(result.username)
11515 $html.find('.scamNotice').remove()
11516 let $avatar = $html.clone();
11517 $avatar.prepend($(`<img class="smallCompleteAvatar" src="${result.avatar}" style="float: left;margin-top: 2px;margin-right: 5px;border-radius: 17px"/>`))
11518 return $avatar.prop('outerHTML');
11519 },
11520
11521 initializeOld: function (value) {
11522 let elements = [];
11523 for (let i = 0; i < value.length; i++) {
11524 value[i] = XenForo.htmlspecialchars(value[i])
11525 elements.push(value[i]);
11526 let match = this.findMatch(value[i]);
11527 let newOption = new Option(match ? this.formatAvatar(this.$element.data('result').results[match]) : value[i], value[i], false, false);
11528 let hit;
11529 this.$select.find('option').each(function () {
11530 if ($(this).attr('value') === value[i]) {
11531 if (match) $(this).remove()
11532 else hit = true
11533 }
11534 })
11535 if (!hit)
11536 this.$select.append(newOption).trigger('change');
11537 }
11538 this.$select.val(elements);
11539 this.$select.trigger('change');
11540 this.bind();
11541 },
11542
11543 insertAutoComplete: function (name, html, avatar) {
11544 let elements = [];
11545 let data = this.$select.select2('data');
11546 for (let el = 0; el < data.length; el++) {
11547 if (data[el].selected) elements.push(data[el].id);
11548 }
11549 elements.push(name);
11550 if (!this.$select.find('option').filter((_, element) => { return $(element).attr('value') === avatar || $(element).attr('value') === name }).length) {
11551 let newOption = new Option(avatar, name, false, false);
11552 this.$select.append(newOption).trigger('change');
11553 }
11554 this.$select.val(elements);
11555 this.$select.trigger('change');
11556 this.$element.val(elements.join(","))
11557 this.lastAcLookup = '';
11558 this.$parent.find('.select2-search__field').val('');
11559 setTimeout(() => { this.bind() })
11560 },
11561
11562 bind: function () {
11563 this.$parent.find('.select2-search__field').off('focus keydown keyup').on('focus keydown', function (e) {
11564 if (e.type === "focus" && this.$parent.find('.select2-search__field').css('width') && !this.$parent.find('.select2-search__field').attr('width')) {
11565 this.$parent.find('.select2-search__field').attr('width', 'set');
11566 let padding = parseInt(window.getComputedStyle(this.$parent.find('.select2-selection__rendered').get(0), null).getPropertyValue('padding-left'));
11567 if (this.$parent.find('.select2-search__field').width() - padding > 0) {
11568 this.$parent.find('.select2-search__field').css('width', this.$parent.find('.select2-search__field').width() - padding + 'px');
11569 }
11570 }
11571 var prevent = true,
11572 acResults = this.acResults;
11573
11574 if (e.keyCode === 8 && this.$select.select2('data').length && !this.$parent.find('.select2-search__field').val()) {
11575 e.stopPropagation();
11576 e.stopImmediatePropagation();
11577 e.preventDefault();
11578 // Remove last element and put username to input
11579 let elements = [];
11580 let data = this.$select.select2('data');
11581 for (let el = 0; el < data.length; el++) {
11582 if (data[el].selected) elements.push(data[el].id);
11583 }
11584 this.$parent.find('.select2-search__field').val(elements.pop());
11585 this.$select.val(elements);
11586 this.$select.trigger('change');
11587 this.$element.val(elements.join(","))
11588 this.lastAcLookup = '';
11589 setTimeout(() => { this.bind() })
11590 }
11591
11592 if (!acResults.isVisible()) {
11593 return;
11594 }
11595
11596 switch (e.keyCode) {
11597 case 8:
11598 this.lastAcLookup = '';
11599 prevent = false; // backspace
11600 case 40:
11601 acResults.selectResult(1);
11602 break; // down
11603 case 38:
11604 acResults.selectResult(-1);
11605 break; // up
11606 case 27:
11607 acResults.hideResults();
11608 break; // esc
11609 case 13:
11610 if (!e.shiftKey && acResults.selectedResult !== -1) {
11611 // Shift isn't pressed
11612 acResults.insertSelectedResult();
11613 break;
11614 }
11615 // Shift is pressed, allow user to break line
11616 this.lastAcLookup = '';
11617 acResults.hideResults(); // enter
11618 default:
11619 prevent = false;
11620 }
11621
11622 if (prevent) {
11623 e.stopPropagation();
11624 e.stopImmediatePropagation();
11625 e.preventDefault();
11626 }
11627 }.bind(this)).on('keyup', function (e) {
11628 if ([13, 17, 27, 38, 40].indexOf(e.keyCode) !== -1 || e.ctrlKey && e.keyCode === 86) {
11629 return
11630 }
11631 if (this.$parent.find('.select2-search__field').val()) this.triggerAutoComplete(this.$parent.find('.select2-search__field').val().replaceAll("@", "").trim());
11632 }.bind(this)).on('blur', function () {
11633 if (this.$parent.find('.select2-search__field').css('width') && !this.$parent.find('.select2-search__field').attr('width')) {
11634 this.$parent.find('.select2-search__field').attr('width', 'set');
11635 let padding = parseInt(window.getComputedStyle(this.$parent.find('.select2-selection__rendered').get(0), null).getPropertyValue('padding-left'));
11636 if (this.$parent.find('.select2-search__field').width() - padding > 0) {
11637 this.$parent.find('.select2-search__field').css('width', this.$parent.find('.select2-search__field').width() - padding + 'px');
11638 }
11639 }
11640
11641 setTimeout($.context(this, 'hideAutoComplete'), 300)
11642 }.bind(this)).on('paste', function () {
11643 setTimeout(() => {
11644 this.lastAcLookup = '';
11645 this.triggerAutoComplete(this.$parent.find('.select2-search__field').val().replaceAll("@", "").trim(), true)
11646 });
11647 }.bind(this))
11648 },
11649
11650 triggerAutoComplete: function (name, instant) {
11651 if (this.$element.hasClass('AcSingle') && this.$select.select2('data').length) {
11652 return;
11653 }
11654
11655 this.hideAutoComplete();
11656 this.lastAcLookup = name;
11657 if (name.length > 2 && name.substr(0, 1) !== '[') {
11658 this.acLoadTimer = setTimeout($.context(this, 'autoCompleteLookup'), instant ? 0 : 200);
11659 }
11660 },
11661
11662 autoCompleteLookup: function () {
11663 if (this.acXhr) {
11664 this.acXhr.abort();
11665 }
11666
11667 this.acXhr = XenForo.ajax(
11668 this.$element.attr('data-acurl') || XenForo.AutoComplete.getDefaultUrl(),
11669 { q: this.lastAcLookup },
11670 $.context(this, 'showAutoCompleteResults'),
11671 { global: false, error: false }
11672 );
11673 },
11674
11675 hideAutoComplete: function () {
11676 this.acResults.hideResults();
11677 if (this.acLoadTimer) {
11678 clearTimeout(this.acLoadTimer);
11679 this.acLoadTimer = false;
11680 }
11681 },
11682
11683 showAutoCompleteResults: function (ajaxData, def) {
11684 this.acXhr = false;
11685
11686 if (this.lastAcLookup !== this.$parent.find('.select2-search__field').val().replaceAll("@", "").trim()) {
11687 return;
11688 }
11689
11690 this.acResults.showResults(
11691 this.lastAcLookup,
11692 ajaxData.results,
11693 this.$parent.find('.select2-search__field')
11694 );
11695 },
11696 }
11697
11698 XenForo.register('.ChosenAutoComplete', 'XenForo.EnhancedAutoComplete');
11699
11700 // *********************************************************************
11701
11702 XenForo.Cachify = function () {
11703 if (!navigator.serviceWorker) return;
11704 navigator.serviceWorker.register('/js/lolzteam/sw.js?_v=' + XenForo._jsVersion, { scope: '/' }).then(function (worker) {
11705 if (worker && worker.active)
11706 worker.active.postMessage({
11707 method: 'initCache',
11708 path: '/cache?_xfResponseType=json&_xfToken=' + XenForo._csrfToken
11709 })
11710 }).catch(function (error) {
11711 console.error('Error deploying service worker:', error);
11712 });
11713 };
11714
11715 XenForo.register('html.Cachify', 'XenForo.Cachify');
11716
11717 // *********************************************************************
11718
11719 XenForo.AlertsClear = function ($link) {
11720 var callback = function (ajaxData, textStatus) {
11721 if (ajaxData.error) {
11722 XenForo.hasResponseError(ajaxData, textStatus);
11723 } else {
11724 $('.alerts.Popup').data('XenForo.PopupMenu').reload();
11725 }
11726 };
11727
11728 $link.click(function (e) {
11729 e.preventDefault();
11730 const alertType = $('.alertsTabs').find('.active').data('id');
11731 XenForo.ajax($link.attr('href'), { type: alertType }, callback);
11732 });
11733 };
11734
11735 XenForo.register('a.AlertsClear', 'XenForo.AlertsClear');
11736
11737 // *********************************************************************
11738
11739 XenForo.ClickableTabs = function ($element) {
11740 $element.find('li').on('click', function (e) {
11741 if (e.target.tagName !== "LI" || !$(this).find('a').length) return;
11742 e.preventDefault();
11743 e.stopPropagation();
11744 e.stopImmediatePropagation();
11745
11746 $(this).find('a')[0].click();
11747 })
11748 };
11749
11750 XenForo.register('.tabs', 'XenForo.ClickableTabs');
11751
11752 XenForo.AlertReader = function ($element) {
11753 $element.parent().on('click mouseover', function () {
11754 localStorage.setItem('read_notifies', + new Date());
11755 });
11756
11757 $(window).on('storage', function (e) {
11758 if (e.originalEvent.key === 'read_notifies') {
11759 localStorage.removeItem('read_notifies');
11760 const badge = $("#AlertsMenu_Counter");
11761 if (!badge.hasClass('Zero')) {
11762 badge.addClass('Zero');
11763 }
11764 }
11765 })
11766 };
11767
11768 /**
11769 * не юзайте больше обычный tippy, заебали пастить настройки
11770 * обдрочитесь же заменять когда обновлять будете
11771 * ваще по хорошему надо все отрефакторить на эту функу
11772 */
11773 XenForo.tippy = function (target, options = {}, preset = 'tooltip') {
11774 if (typeof preset === 'string') preset = preset.split(' ')
11775
11776 const presets = {
11777 default: {
11778 zIndex: $(target).closest('.xenOverlay').length ? 11111 : 9000, // overlay fix
11779 boundary: 'viewport',
11780 distance: 5,
11781 arrow: true,
11782 animation: 'shift-toward',
11783 trigger: XenForo.isTouchBrowser() ? 'click' : 'mouseenter focus',
11784 appendTo: $(target).closest('.modal, body')[0] // https://zelenka.guru/threads/4730409/
11785 },
11786 tooltip: {
11787 hideOnClick: true,
11788 },
11789 popup: {
11790 theme: 'popup',
11791 placement: 'top',
11792 interactive: true,
11793 hideOnClick: XenForo.isTouchBrowser()
11794 }
11795 }
11796
11797 return tippy(target, $.extend(
11798 presets.default,
11799 ...preset.map(p => presets[p]),
11800 options
11801 ))
11802 }
11803
11804 if (supports_html5_storage()) {
11805 XenForo.register('a[href="account/alerts"]', 'XenForo.AlertReader');
11806 }
11807
11808 XenForo.ContactDetailsForm = function ($element) {
11809 const secretAnswer = $element.find("#secretAnswer");
11810
11811 $element.find('input[type="email"]').on('input', () => {
11812 if (!secretAnswer.is(':visible')) {
11813 secretAnswer.show();
11814 }
11815 });
11816 }
11817
11818 XenForo.register('form.ContactDetailsForm', 'XenForo.ContactDetailsForm');
11819}(jQuery);