Add possibility to sort labels in ACP
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WCF.js
1 "use strict";
2
3 /**
4 * Class and function collection for WCF.
5 *
6 * Major Contributors: Markus Bartz, Tim Duesterhus, Matthias Schmidt and Marcel Werk
7 *
8 * @author Alexander Ebert
9 * @copyright 2001-2015 WoltLab GmbH
10 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
11 */
12
13 (function() {
14 // store original implementation
15 var $jQueryData = jQuery.fn.data;
16
17 /**
18 * Override jQuery.fn.data() to support custom 'ID' suffix which will
19 * be translated to '-id' at runtime.
20 *
21 * @see jQuery.fn.data()
22 */
23 jQuery.fn.data = function(key, value) {
24 if (key) {
25 switch (typeof key) {
26 case 'object':
27 for (var $key in key) {
28 if ($key.match(/ID$/)) {
29 var $value = key[$key];
30 delete key[$key];
31
32 $key = $key.replace(/ID$/, '-id');
33 key[$key] = $value;
34 }
35 }
36
37 arguments[0] = key;
38 break;
39
40 case 'string':
41 if (key.match(/ID$/)) {
42 arguments[0] = key.replace(/ID$/, '-id');
43 }
44 break;
45 }
46 }
47
48 // call jQuery's own data method
49 var $data = $jQueryData.apply(this, arguments);
50
51 // handle .data() call without arguments
52 if (key === undefined) {
53 for (var $key in $data) {
54 if ($key.match(/Id$/)) {
55 $data[$key.replace(/Id$/, 'ID')] = $data[$key];
56 delete $data[$key];
57 }
58 }
59 }
60
61 return $data;
62 };
63
64 // provide a sane window.console implementation
65 if (!window.console) window.console = { };
66 var consoleProperties = [ "log",/* "debug",*/ "info", "warn", "exception", "assert", "dir", "dirxml", "trace", "group", "groupEnd", "groupCollapsed", "profile", "profileEnd", "count", "clear", "time", "timeEnd", "timeStamp", "table", "error" ];
67 for (var i = 0; i < consoleProperties.length; i++) {
68 if (typeof (console[consoleProperties[i]]) === 'undefined') {
69 console[consoleProperties[i]] = function () { };
70 }
71 }
72
73 if (typeof(console.debug) === 'undefined') {
74 // forward console.debug to console.log (IE9)
75 console.debug = function(string) { console.log(string); };
76 }
77 })();
78
79
80
81 /**
82 * Provides a hashCode() method for strings, similar to Java's String.hashCode().
83 *
84 * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
85 */
86 String.prototype.hashCode = function() {
87 var $char;
88 var $hash = 0;
89
90 if (this.length) {
91 for (var $i = 0, $length = this.length; $i < $length; $i++) {
92 $char = this.charCodeAt($i);
93 $hash = (($hash << 5) - $hash) + $char;
94 $hash = $hash & $hash; // convert to 32bit integer
95 }
96 }
97
98 return $hash;
99 };
100
101 /**
102 * Adds a Fisher-Yates shuffle algorithm for arrays.
103 *
104 * @see http://stackoverflow.com/a/2450976
105 */
106 window.shuffle = function(array) {
107 var currentIndex = array.length, temporaryValue, randomIndex;
108
109 // While there remain elements to shuffle...
110 while (0 !== currentIndex) {
111 // Pick a remaining element...
112 randomIndex = Math.floor(Math.random() * currentIndex);
113 currentIndex -= 1;
114
115 // And swap it with the current element.
116 temporaryValue = array[currentIndex];
117 array[currentIndex] = array[randomIndex];
118 array[randomIndex] = temporaryValue;
119 }
120
121 return this;
122 };
123
124 /**
125 * User-Agent based browser detection and touch detection.
126 */
127 (function(jQuery) {
128 var ua = navigator.userAgent.toLowerCase();
129 var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
130 /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
131 /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
132 /(msie) ([\w.]+)/.exec( ua ) ||
133 ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
134 [];
135
136 var matched = {
137 browser: match[ 1 ] || "",
138 version: match[ 2 ] || "0"
139 };
140 var browser = {};
141
142 if ( matched.browser ) {
143 browser[ matched.browser ] = true;
144 browser.version = matched.version;
145 }
146
147 // Chrome is Webkit, but Webkit is also Safari.
148 if ( browser.chrome ) {
149 browser.webkit = true;
150 } else if ( browser.webkit ) {
151 browser.safari = true;
152 }
153
154 jQuery.browser = jQuery.browser || { };
155 jQuery.browser = $.extend(jQuery.browser, browser);
156 jQuery.browser.touch = (!!('ontouchstart' in window) || (!!('msMaxTouchPoints' in window.navigator) && window.navigator.msMaxTouchPoints > 0));
157
158 // detect smartphones
159 jQuery.browser.smartphone = ($('html').css('caption-side') == 'bottom');
160
161 // properly detect IE11
162 if (jQuery.browser.mozilla && ua.match(/trident/)) {
163 jQuery.browser.mozilla = false;
164 jQuery.browser.msie = true;
165 }
166
167 // detect iOS devices
168 jQuery.browser.iOS = /\((ipad|iphone|ipod);/.test(ua);
169 if (jQuery.browser.iOS) {
170 $('html').addClass('iOS');
171 }
172
173 // dectect Android
174 jQuery.browser.android = (ua.indexOf('android') !== -1);
175
176 // allow plugins to detect the used editor, value should be the same as the $.browser.<editorName> key
177 jQuery.browser.editor = 'redactor';
178
179 // CKEditor support (removed in WCF 2.1), do NOT remove this variable for the sake for compatibility
180 jQuery.browser.ckeditor = false;
181
182 // Redactor support
183 jQuery.browser.redactor = true;
184
185 // work-around for zoom bug on iOS when using .focus()
186 if (jQuery.browser.iOS) {
187 jQuery.fn.focus = function(data, fn) {
188 return arguments.length > 0 ? this.on('focus', null, data, fn) : this.trigger('focus');
189 };
190 }
191 })(jQuery);
192
193 /**
194 * Initialize WCF namespace
195 */
196 // non strict equals by intent
197 if (window.WCF == null) window.WCF = { };
198
199 /**
200 * Extends jQuery with additional methods.
201 */
202 $.extend(true, {
203 /**
204 * Removes the given value from the given array and returns the array.
205 *
206 * @param array array
207 * @param mixed element
208 * @return array
209 */
210 removeArrayValue: function(array, value) {
211 return $.grep(array, function(element, index) {
212 return value !== element;
213 });
214 },
215
216 /**
217 * Escapes an ID to work with jQuery selectors.
218 *
219 * @see http://docs.jquery.com/Frequently_Asked_Questions#How_do_I_select_an_element_by_an_ID_that_has_characters_used_in_CSS_notation.3F
220 * @param string id
221 * @return string
222 */
223 wcfEscapeID: function(id) {
224 return id.replace(/(:|\.)/g, '\\$1');
225 },
226
227 /**
228 * Returns true if given ID exists within DOM.
229 *
230 * @param string id
231 * @return boolean
232 */
233 wcfIsset: function(id) {
234 return !!$('#' + $.wcfEscapeID(id)).length;
235 },
236
237 /**
238 * Returns the length of an object.
239 *
240 * @param object targetObject
241 * @return integer
242 */
243 getLength: function(targetObject) {
244 var $length = 0;
245
246 for (var $key in targetObject) {
247 if (targetObject.hasOwnProperty($key)) {
248 $length++;
249 }
250 }
251
252 return $length;
253 }
254 });
255
256 /**
257 * Extends jQuery's chainable methods.
258 */
259 $.fn.extend({
260 /**
261 * Returns tag name of first jQuery element.
262 *
263 * @returns string
264 */
265 getTagName: function() {
266 return (this.length) ? this.get(0).tagName.toLowerCase() : '';
267 },
268
269 /**
270 * Returns the dimensions for current element.
271 *
272 * @see http://api.jquery.com/hidden-selector/
273 * @param string type
274 * @return object
275 */
276 getDimensions: function(type) {
277 var css = { };
278 var dimensions = { };
279 var wasHidden = false;
280
281 // show element to retrieve dimensions and restore them later
282 if (this.is(':hidden')) {
283 css = WCF.getInlineCSS(this);
284
285 wasHidden = true;
286
287 this.css({
288 display: 'block',
289 visibility: 'hidden'
290 });
291 }
292
293 switch (type) {
294 case 'inner':
295 dimensions = {
296 height: this.innerHeight(),
297 width: this.innerWidth()
298 };
299 break;
300
301 case 'outer':
302 dimensions = {
303 height: this.outerHeight(),
304 width: this.outerWidth()
305 };
306 break;
307
308 default:
309 dimensions = {
310 height: this.height(),
311 width: this.width()
312 };
313 break;
314 }
315
316 // restore previous settings
317 if (wasHidden) {
318 WCF.revertInlineCSS(this, css, [ 'display', 'visibility' ]);
319 }
320
321 return dimensions;
322 },
323
324 /**
325 * Returns the offsets for current element, defaults to position
326 * relative to document.
327 *
328 * @see http://api.jquery.com/hidden-selector/
329 * @param string type
330 * @return object
331 */
332 getOffsets: function(type) {
333 var css = { };
334 var offsets = { };
335 var wasHidden = false;
336
337 // show element to retrieve dimensions and restore them later
338 if (this.is(':hidden')) {
339 css = WCF.getInlineCSS(this);
340 wasHidden = true;
341
342 this.css({
343 display: 'block',
344 visibility: 'hidden'
345 });
346 }
347
348 switch (type) {
349 case 'offset':
350 offsets = this.offset();
351 break;
352
353 case 'position':
354 default:
355 offsets = this.position();
356 break;
357 }
358
359 // restore previous settings
360 if (wasHidden) {
361 WCF.revertInlineCSS(this, css, [ 'display', 'visibility' ]);
362 }
363
364 return offsets;
365 },
366
367 /**
368 * Changes element's position to 'absolute' or 'fixed' while maintaining it's
369 * current position relative to viewport. Optionally removes element from
370 * current DOM-node and moving it into body-element (useful for drag & drop)
371 *
372 * @param boolean rebase
373 * @return object
374 */
375 makePositioned: function(position, rebase) {
376 if (position != 'absolute' && position != 'fixed') {
377 position = 'absolute';
378 }
379
380 var $currentPosition = this.getOffsets('position');
381 this.css({
382 position: position,
383 left: $currentPosition.left,
384 margin: 0,
385 top: $currentPosition.top
386 });
387
388 if (rebase) {
389 this.remove().appentTo('body');
390 }
391
392 return this;
393 },
394
395 /**
396 * Disables a form element.
397 *
398 * @return jQuery
399 */
400 disable: function() {
401 return this.attr('disabled', 'disabled');
402 },
403
404 /**
405 * Enables a form element.
406 *
407 * @return jQuery
408 */
409 enable: function() {
410 return this.removeAttr('disabled');
411 },
412
413 /**
414 * Returns the element's id. If none is set, a random unique
415 * ID will be assigned.
416 *
417 * @return string
418 */
419 wcfIdentify: function() {
420 return window.bc_wcfDomUtil.identify(this[0]);
421 },
422
423 /**
424 * Returns the caret position of current element. If the element
425 * does not equal input[type=text], input[type=password] or
426 * textarea, -1 is returned.
427 *
428 * @return integer
429 */
430 getCaret: function() {
431 if (this.is('input')) {
432 if (this.attr('type') != 'text' && this.attr('type') != 'password') {
433 return -1;
434 }
435 }
436 else if (!this.is('textarea')) {
437 return -1;
438 }
439
440 var $position = 0;
441 var $element = this.get(0);
442 if (document.selection) { // IE 8
443 // set focus to enable caret on this element
444 this.focus();
445
446 var $selection = document.selection.createRange();
447 $selection.moveStart('character', -this.val().length);
448 $position = $selection.text.length;
449 }
450 else if ($element.selectionStart || $element.selectionStart == '0') { // Opera, Chrome, Firefox, Safari, IE 9+
451 $position = parseInt($element.selectionStart);
452 }
453
454 return $position;
455 },
456
457 /**
458 * Sets the caret position of current element. If the element
459 * does not equal input[type=text], input[type=password] or
460 * textarea, false is returned.
461 *
462 * @param integer position
463 * @return boolean
464 */
465 setCaret: function (position) {
466 if (this.is('input')) {
467 if (this.attr('type') != 'text' && this.attr('type') != 'password') {
468 return false;
469 }
470 }
471 else if (!this.is('textarea')) {
472 return false;
473 }
474
475 var $element = this.get(0);
476
477 // set focus to enable caret on this element
478 this.focus();
479 if (document.selection) { // IE 8
480 var $selection = document.selection.createRange();
481 $selection.moveStart('character', position);
482 $selection.moveEnd('character', 0);
483 $selection.select();
484 }
485 else if ($element.selectionStart || $element.selectionStart == '0') { // Opera, Chrome, Firefox, Safari, IE 9+
486 $element.selectionStart = position;
487 $element.selectionEnd = position;
488 }
489
490 return true;
491 },
492
493 /**
494 * Shows an element by sliding and fading it into viewport.
495 *
496 * @param string direction
497 * @param object callback
498 * @param integer duration
499 * @returns jQuery
500 */
501 wcfDropIn: function(direction, callback, duration) {
502 if (!direction) direction = 'up';
503 if (!duration || !parseInt(duration)) duration = 200;
504
505 return this.show(WCF.getEffect(this, 'drop'), { direction: direction }, duration, callback);
506 },
507
508 /**
509 * Hides an element by sliding and fading it out the viewport.
510 *
511 * @param string direction
512 * @param object callback
513 * @param integer duration
514 * @returns jQuery
515 */
516 wcfDropOut: function(direction, callback, duration) {
517 if (!direction) direction = 'down';
518 if (!duration || !parseInt(duration)) duration = 200;
519
520 return this.hide(WCF.getEffect(this, 'drop'), { direction: direction }, duration, callback);
521 },
522
523 /**
524 * Shows an element by blinding it up.
525 *
526 * @param string direction
527 * @param object callback
528 * @param integer duration
529 * @returns jQuery
530 */
531 wcfBlindIn: function(direction, callback, duration) {
532 if (!direction) direction = 'vertical';
533 if (!duration || !parseInt(duration)) duration = 200;
534
535 return this.show(WCF.getEffect(this, 'blind'), { direction: direction }, duration, callback);
536 },
537
538 /**
539 * Hides an element by blinding it down.
540 *
541 * @param string direction
542 * @param object callback
543 * @param integer duration
544 * @returns jQuery
545 */
546 wcfBlindOut: function(direction, callback, duration) {
547 if (!direction) direction = 'vertical';
548 if (!duration || !parseInt(duration)) duration = 200;
549
550 return this.hide(WCF.getEffect(this, 'blind'), { direction: direction }, duration, callback);
551 },
552
553 /**
554 * Highlights an element.
555 *
556 * @param object options
557 * @param object callback
558 * @returns jQuery
559 */
560 wcfHighlight: function(options, callback) {
561 return this.effect('highlight', options, 600, callback);
562 },
563
564 /**
565 * Shows an element by fading it in.
566 *
567 * @param object callback
568 * @param integer duration
569 * @returns jQuery
570 */
571 wcfFadeIn: function(callback, duration) {
572 if (!duration || !parseInt(duration)) duration = 200;
573
574 return this.show(WCF.getEffect(this, 'fade'), { }, duration, callback);
575 },
576
577 /**
578 * Hides an element by fading it out.
579 *
580 * @param object callback
581 * @param integer duration
582 * @returns jQuery
583 */
584 wcfFadeOut: function(callback, duration) {
585 if (!duration || !parseInt(duration)) duration = 200;
586
587 return this.hide(WCF.getEffect(this, 'fade'), { }, duration, callback);
588 },
589
590 /**
591 * Returns a CSS property as raw number.
592 *
593 * @param string property
594 */
595 cssAsNumber: function(property) {
596 if (this.length) {
597 var $property = this.css(property);
598 if ($property !== undefined) {
599 return parseInt($property.replace(/px$/, ''));
600 }
601 }
602
603 return 0;
604 },
605 /**
606 * @deprecated Use perfectScrollbar directly.
607 *
608 * This is taken from the jQuery adaptor of perfect scrollbar.
609 * Copyright (c) 2015 Hyunje Alex Jun and other contributors
610 * Licensed under the MIT License
611 */
612 perfectScrollbar: function (settingOrCommand) {
613 var ps = require('perfect-scrollbar');
614
615 return this.each(function () {
616 if (typeof settingOrCommand === 'object' ||
617 typeof settingOrCommand === 'undefined') {
618 // If it's an object or none, initialize.
619 var settings = settingOrCommand;
620 if (!$(this).data('psID'))
621 ps.initialize(this, settings);
622 } else {
623 // Unless, it may be a command.
624 var command = settingOrCommand;
625
626 if (command === 'update') {
627 ps.update(this);
628 } else if (command === 'destroy') {
629 ps.destroy(this);
630 }
631 }
632
633 return jQuery(this);
634 });
635 }
636 });
637
638 /**
639 * WoltLab Community Framework core methods
640 */
641 $.extend(WCF, {
642 /**
643 * count of active dialogs
644 * @var integer
645 */
646 activeDialogs: 0,
647
648 /**
649 * Counter for dynamic element ids
650 *
651 * @var integer
652 */
653 _idCounter: 0,
654
655 /**
656 * Returns a dynamically created id.
657 *
658 * @see https://github.com/sstephenson/prototype/blob/5e5cfff7c2c253eaf415c279f9083b4650cd4506/src/prototype/dom/dom.js#L1789
659 * @return string
660 */
661 getRandomID: function() {
662 return window.bc_wcfDomUtil.getUniqueId();
663 },
664
665 /**
666 * Wrapper for $.inArray which returns boolean value instead of
667 * index value, similar to PHP's in_array().
668 *
669 * @param mixed needle
670 * @param array haystack
671 * @return boolean
672 */
673 inArray: function(needle, haystack) {
674 return ($.inArray(needle, haystack) != -1);
675 },
676
677 /**
678 * Adjusts effect for partially supported elements.
679 *
680 * @param jQuery object
681 * @param string effect
682 * @return string
683 */
684 getEffect: function(object, effect) {
685 // most effects are not properly supported on table rows, use highlight instead
686 if (object.is('tr')) {
687 return 'highlight';
688 }
689
690 return effect;
691 },
692
693 /**
694 * Returns inline CSS for given element.
695 *
696 * @param jQuery element
697 * @return object
698 */
699 getInlineCSS: function(element) {
700 var $inlineStyles = { };
701 var $style = element.attr('style');
702
703 // no style tag given or empty
704 if (!$style) {
705 return { };
706 }
707
708 $style = $style.split(';');
709 for (var $i = 0, $length = $style.length; $i < $length; $i++) {
710 var $fragment = $.trim($style[$i]);
711 if ($fragment == '') {
712 continue;
713 }
714
715 $fragment = $fragment.split(':');
716 $inlineStyles[$.trim($fragment[0])] = $.trim($fragment[1]);
717 }
718
719 return $inlineStyles;
720 },
721
722 /**
723 * Reverts inline CSS or negates a previously set property.
724 *
725 * @param jQuery element
726 * @param object inlineCSS
727 * @param array<string> targetProperties
728 */
729 revertInlineCSS: function(element, inlineCSS, targetProperties) {
730 for (var $i = 0, $length = targetProperties.length; $i < $length; $i++) {
731 var $property = targetProperties[$i];
732
733 // revert inline CSS
734 if (inlineCSS[$property]) {
735 element.css($property, inlineCSS[$property]);
736 }
737 else {
738 // negate inline CSS
739 element.css($property, '');
740 }
741 }
742 },
743
744 /**
745 * @deprecated Use WoltLab/WCF/Core.getUuid().
746 */
747 getUUID: function() {
748 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
749 var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
750 return v.toString(16);
751 });
752 },
753
754 /**
755 * Converts a base64 encoded file into a native Blob.
756 *
757 * @param string base64data
758 * @param string contentType
759 * @param integer sliceSize
760 * @return Blob
761 */
762 base64toBlob: function(base64data, contentType, sliceSize) {
763 contentType = contentType || '';
764 sliceSize = sliceSize || 512;
765
766 var $byteCharacters = atob(base64data);
767 var $byteArrays = [ ];
768
769 for (var $offset = 0; $offset < $byteCharacters.length; $offset += sliceSize) {
770 var $slice = $byteCharacters.slice($offset, $offset + sliceSize);
771
772 var $byteNumbers = new Array($slice.length);
773 for (var $i = 0; $i < $slice.length; $i++) {
774 $byteNumbers[$i] = $slice.charCodeAt($i);
775 }
776
777 var $byteArray = new Uint8Array($byteNumbers);
778 $byteArrays.push($byteArray);
779 }
780
781 return new Blob($byteArrays, { type: contentType });
782 },
783
784 /**
785 * Converts legacy URLs to the URL schema used by WCF 2.1.
786 *
787 * @param string url
788 * @return string
789 */
790 convertLegacyURL: function(url) {
791 if (URL_LEGACY_MODE) {
792 return url;
793 }
794
795 return url.replace(/^index\.php\/(.*?)\/\?/, function(match, controller) {
796 var $parts = controller.split(/([A-Z][a-z0-9]+)/);
797 var $controller = '';
798 for (var $i = 0, $length = $parts.length; $i < $length; $i++) {
799 var $part = $parts[$i].trim();
800 if ($part.length) {
801 if ($controller.length) $controller += '-';
802 $controller += $part.toLowerCase();
803 }
804 }
805
806 return 'index.php?' + $controller + '/&';
807 });
808 }
809 });
810
811 /**
812 * Browser related functions.
813 */
814 WCF.Browser = {
815 /**
816 * determines if browser is chrome
817 * @var boolean
818 */
819 _isChrome: null,
820
821 /**
822 * Returns true, if browser is Chrome, Chromium or using GoogleFrame for Internet Explorer.
823 *
824 * @return boolean
825 */
826 isChrome: function() {
827 if (this._isChrome === null) {
828 this._isChrome = false;
829 if (/chrom(e|ium)/.test(navigator.userAgent.toLowerCase())) {
830 this._isChrome = true;
831 }
832 }
833
834 return this._isChrome;
835 }
836 };
837
838 /**
839 * Dropdown API
840 *
841 * @deprecated 2.2 - please use `Ui/SimpleDropdown` instead
842 */
843 WCF.Dropdown = {
844 /**
845 * Initializes dropdowns.
846 */
847 init: function(api) {
848 window.bc_wcfSimpleDropdown.initAll();
849 },
850
851 /**
852 * Initializes a dropdown.
853 *
854 * @param jQuery button
855 * @param boolean isLazyInitialization
856 */
857 initDropdown: function(button, isLazyInitialization) {
858 window.bc_wcfSimpleDropdown.init(button[0], isLazyInitialization);
859 },
860
861 /**
862 * Removes the dropdown with the given container id.
863 *
864 * @param string containerID
865 */
866 removeDropdown: function(containerID) {
867 window.bc_wcfSimpleDropdown.remove(containerID);
868 },
869
870 /**
871 * Initializes a dropdown fragment which behaves like a usual dropdown
872 * but is not controlled by a trigger element.
873 *
874 * @param jQuery dropdown
875 * @param jQuery dropdownMenu
876 */
877 initDropdownFragment: function(dropdown, dropdownMenu) {
878 window.bc_wcfSimpleDropdown.initFragment(dropdown[0], dropdownMenu[0]);
879 },
880
881 /**
882 * Registers a callback notified upon dropdown state change.
883 *
884 * @param string identifier
885 * @var object callback
886 */
887 registerCallback: function(identifier, callback) {
888 window.bc_wcfSimpleDropdown.registerCallback(identifier, callback);
889 },
890
891 /**
892 * Toggles a dropdown.
893 *
894 * @param object event
895 * @param string targetID
896 */
897 _toggle: function(event, targetID) {
898 window.bc_wcfSimpleDropdown._toggle(event, targetID);
899 },
900
901 /**
902 * Toggles a dropdown.
903 *
904 * @param string containerID
905 */
906 toggleDropdown: function(containerID) {
907 window.bc_wcfSimpleDropdown._toggle(null, containerID);
908 },
909
910 /**
911 * Returns dropdown by container id.
912 *
913 * @param string containerID
914 * @return jQuery
915 */
916 getDropdown: function(containerID) {
917 var dropdown = window.bc_wcfSimpleDropdown.getDropdown(containerID);
918
919 return (dropdown) ? $(dropdown) : null;
920 },
921
922 /**
923 * Returns dropdown menu by container id.
924 *
925 * @param string containerID
926 * @return jQuery
927 */
928 getDropdownMenu: function(containerID) {
929 var menu = window.bc_wcfSimpleDropdown.getDropdownMenu(containerID);
930
931 return (menu) ? $(menu) : null;
932 },
933
934 /**
935 * Sets alignment for given container id.
936 *
937 * @param string containerID
938 */
939 setAlignmentByID: function(containerID) {
940 window.bc_wcfSimpleDropdown.setAlignmentById(containerID);
941 },
942
943 /**
944 * Sets alignment for dropdown.
945 *
946 * @param jQuery dropdown
947 * @param jQuery dropdownMenu
948 */
949 setAlignment: function(dropdown, dropdownMenu) {
950 window.bc_wcfSimpleDropdown.setAlignment(dropdown[0], dropdownMenu[0]);
951 },
952
953 /**
954 * Closes all dropdowns.
955 */
956 _closeAll: function() {
957 window.bc_wcfSimpleDropdown.closeAll();
958 },
959
960 /**
961 * Closes a dropdown without notifying callbacks.
962 *
963 * @param string containerID
964 */
965 close: function(containerID) {
966 window.bc_wcfSimpleDropdown.close(containerID);
967 },
968
969 /**
970 * Destroies an existing dropdown menu.
971 *
972 * @param string containerID
973 * @return boolean
974 */
975 destroy: function(containerID) {
976 window.bc_wcfSimpleDropdown.destroy(containerID);
977 }
978 };
979
980 /**
981 * Namespace for interactive dropdowns.
982 */
983 WCF.Dropdown.Interactive = { };
984
985 /**
986 * General interface to create and manage interactive dropdowns.
987 */
988 WCF.Dropdown.Interactive.Handler = {
989 /**
990 * global container for interactive dropdowns
991 * @var jQuery
992 */
993 _dropdownContainer: null,
994
995 /**
996 * list of dropdown instances by identifier
997 * @var object<WCF.Dropdown.Interactive.Instance>
998 */
999 _dropdownMenus: { },
1000
1001 /**
1002 * Creates a new interactive dropdown instance.
1003 *
1004 * @param jQuery triggerElement
1005 * @param string identifier
1006 * @param object options
1007 * @return WCF.Dropdown.Interactive.Instance
1008 */
1009 create: function(triggerElement, identifier, options) {
1010 if (this._dropdownContainer === null) {
1011 this._dropdownContainer = $('<div class="dropdownMenuContainer" />').appendTo(document.body);
1012 WCF.CloseOverlayHandler.addCallback('WCF.Dropdown.Interactive.Handler', $.proxy(this.closeAll, this));
1013 window.addEventListener('scroll', this.closeAll.bind(this));
1014 }
1015
1016 var $instance = new WCF.Dropdown.Interactive.Instance(this._dropdownContainer, triggerElement, identifier, options);
1017 this._dropdownMenus[identifier] = $instance;
1018
1019 return $instance;
1020 },
1021
1022 /**
1023 * Opens an interactive dropdown, returns false if identifier is unknown.
1024 *
1025 * @param string identifier
1026 * @return boolean
1027 */
1028 open: function(identifier) {
1029 if (this._dropdownMenus[identifier]) {
1030 this._dropdownMenus[identifier].open();
1031
1032 return true;
1033 }
1034
1035 return false;
1036 },
1037
1038 /**
1039 * Closes an interactive dropdown, returns false if identifier is unknown.
1040 *
1041 * @param string identifier
1042 * @return boolean
1043 */
1044 close: function(identifier) {
1045 if (this._dropdownMenus[identifier]) {
1046 this._dropdownMenus[identifier].close();
1047
1048 return true;
1049 }
1050
1051 return false;
1052 },
1053
1054 /**
1055 * Closes all interactive dropdowns.
1056 */
1057 closeAll: function() {
1058 $.each(this._dropdownMenus, function(identifier, instance) {
1059 instance.close();
1060 });
1061 }
1062 };
1063
1064 /**
1065 * Represents and manages a single interactive dropdown instance.
1066 *
1067 * @param jQuery dropdownContainer
1068 * @param jQuery triggerElement
1069 * @param string identifier
1070 * @param object options
1071 */
1072 WCF.Dropdown.Interactive.Instance = Class.extend({
1073 /**
1074 * dropdown container
1075 * @var jQuery
1076 */
1077 _container: null,
1078
1079 /**
1080 * inner item list
1081 * @var jQuery
1082 */
1083 _itemList: null,
1084
1085 /**
1086 * header link list
1087 * @var jQuery
1088 */
1089 _linkList: null,
1090
1091 /**
1092 * option list
1093 * @var object
1094 */
1095 _options: { },
1096
1097 /**
1098 * arrow pointer
1099 * @var jQuery
1100 */
1101 _pointer: null,
1102
1103 /**
1104 * trigger element
1105 * @var jQuery
1106 */
1107 _triggerElement: null,
1108
1109 /**
1110 * Represents and manages a single interactive dropdown instance.
1111 *
1112 * @param jQuery dropdownContainer
1113 * @param jQuery triggerElement
1114 * @param string identifier
1115 * @param object options
1116 */
1117 init: function(dropdownContainer, triggerElement, identifier, options) {
1118 this._options = options || { };
1119 this._triggerElement = triggerElement;
1120
1121 var $itemContainer = null;
1122 if (options.staticDropdown === true) {
1123 this._container = this._triggerElement.find('.interactiveDropdownStatic:eq(0)').data('source', identifier).click(function(event) { event.stopPropagation(); });
1124 }
1125 else {
1126 this._container = $('<div class="interactiveDropdown" data-source="' + identifier + '" />').click(function(event) { event.stopPropagation(); });
1127
1128 var $header = $('<div class="interactiveDropdownHeader" />').appendTo(this._container);
1129 $('<span class="interactiveDropdownTitle">' + options.title + '</span>').appendTo($header);
1130 this._linkList = $('<ul class="interactiveDropdownLinks inlineList"></ul>').appendTo($header);
1131
1132 $itemContainer = $('<div class="interactiveDropdownItemsContainer" />').appendTo(this._container);
1133 this._itemList = $('<ul class="interactiveDropdownItems" />').appendTo($itemContainer);
1134
1135 $('<a href="' + options.showAllLink + '" class="interactiveDropdownShowAll">' + WCF.Language.get('wcf.user.panel.showAll') + '</a>').appendTo(this._container);
1136 }
1137
1138 this._pointer = $('<span class="elementPointer"><span /></span>').appendTo(this._container);
1139
1140 if (!$.browser.mobile && $itemContainer !== null) {
1141 // use jQuery scrollbar on desktop, mobile browsers have a similar display built-in
1142 $itemContainer.perfectScrollbar({
1143 suppressScrollX: true
1144 });
1145 }
1146
1147 this._container.appendTo(dropdownContainer);
1148 },
1149
1150 /**
1151 * Returns the dropdown container.
1152 *
1153 * @return jQuery
1154 */
1155 getContainer: function() {
1156 return this._container;
1157 },
1158
1159 /**
1160 * Returns the inner item list.
1161 *
1162 * @return jQuery
1163 */
1164 getItemList: function() {
1165 return this._itemList;
1166 },
1167
1168 /**
1169 * Returns the header link list.
1170 *
1171 * @return jQuery
1172 */
1173 getLinkList: function() {
1174 return this._linkList;
1175 },
1176
1177 /**
1178 * Opens the dropdown.
1179 */
1180 open: function() {
1181 WCF.Dropdown._closeAll();
1182
1183 this._container.addClass('open');
1184
1185 this.render();
1186 },
1187
1188 /**
1189 * Closes the dropdown
1190 */
1191 close: function() {
1192 this._container.removeClass('open');
1193 },
1194
1195 /**
1196 * Toggles the dropdown state, returns true if dropdown is open afterwards, else false.
1197 *
1198 * @return boolean
1199 */
1200 toggle: function() {
1201 if (this._container.hasClass('open')) {
1202 this.close();
1203
1204 return false;
1205 }
1206 else {
1207 WCF.Dropdown.Interactive.Handler.closeAll();
1208
1209 this.open();
1210
1211 return true;
1212 }
1213 },
1214
1215 /**
1216 * Resets the inner item list and closes the dropdown.
1217 */
1218 resetItems: function() {
1219 this._itemList.empty();
1220
1221 this.close();
1222 },
1223
1224 /**
1225 * Renders the dropdown.
1226 */
1227 render: function() {
1228 if (window.matchMedia('(max-width: 767px)').matches) {
1229 this._container.css({
1230 bottom: '',
1231 left: '',
1232 right: '',
1233 top: elById('pageHeader').clientHeight + 'px'
1234 });
1235 }
1236 else {
1237 require(['Ui/Alignment'], (function(UiAlignment) {
1238 UiAlignment.set(this._container[0], this._triggerElement[0], {
1239 pointer: true
1240 });
1241 }).bind(this));
1242 }
1243 },
1244
1245 /**
1246 * Rebuilds the desktop scrollbar.
1247 */
1248 rebuildScrollbar: function() {
1249 if (!$.browser.mobile) {
1250 var $itemContainer = this._itemList.parent();
1251
1252 // do NOT use 'update', seems to be broken
1253 $itemContainer.perfectScrollbar('destroy');
1254 $itemContainer.perfectScrollbar({
1255 suppressScrollX: true
1256 });
1257 }
1258 }
1259 });
1260
1261 /**
1262 * Clipboard API
1263 *
1264 * @deprecated 2.2 - please use `WoltLab/WCF/Controller/Clipboard` instead
1265 */
1266 WCF.Clipboard = {
1267 /**
1268 * Initializes the clipboard API.
1269 *
1270 * @param string page
1271 * @param integer hasMarkedItems
1272 * @param object actionObjects
1273 * @param integer pageObjectID
1274 */
1275 init: function(page, hasMarkedItems, actionObjects, pageObjectID) {
1276 require(['WoltLab/WCF/Controller/Clipboard'], function(ControllerClipboard) {
1277 ControllerClipboard.setup({
1278 hasMarkedItems: (hasMarkedItems > 0),
1279 pageClassName: page,
1280 pageObjectId: pageObjectID
1281 });
1282 });
1283 },
1284
1285 /**
1286 * Reloads the list of marked items.
1287 */
1288 reload: function() {
1289 require(['WoltLab/WCF/Controller/Clipboard'], function(ControllerClipboard) {
1290 ControllerClipboard.reload();
1291 });
1292 }
1293 };
1294
1295 /**
1296 * @deprecated Use WoltLab/WCF/Timer/Repeating
1297 */
1298 WCF.PeriodicalExecuter = Class.extend({
1299 /**
1300 * callback for each execution cycle
1301 * @var object
1302 */
1303 _callback: null,
1304
1305 /**
1306 * interval
1307 * @var integer
1308 */
1309 _delay: 0,
1310
1311 /**
1312 * interval id
1313 * @var integer
1314 */
1315 _intervalID: null,
1316
1317 /**
1318 * execution state
1319 * @var boolean
1320 */
1321 _isExecuting: false,
1322
1323 /**
1324 * Initializes a periodical executer.
1325 *
1326 * @param function callback
1327 * @param integer delay
1328 */
1329 init: function(callback, delay) {
1330 if (!$.isFunction(callback)) {
1331 console.debug('[WCF.PeriodicalExecuter] Given callback is invalid, aborting.');
1332 return;
1333 }
1334
1335 this._callback = callback;
1336 this._interval = delay;
1337 this.resume();
1338 },
1339
1340 /**
1341 * Executes callback.
1342 */
1343 _execute: function() {
1344 if (!this._isExecuting) {
1345 try {
1346 this._isExecuting = true;
1347 this._callback(this);
1348 this._isExecuting = false;
1349 }
1350 catch (e) {
1351 this._isExecuting = false;
1352 throw e;
1353 }
1354 }
1355 },
1356
1357 /**
1358 * Terminates loop.
1359 */
1360 stop: function() {
1361 if (!this._intervalID) {
1362 return;
1363 }
1364
1365 clearInterval(this._intervalID);
1366 },
1367
1368 /**
1369 * Resumes the interval-based callback execution.
1370 *
1371 * @deprecated 2.1 - use restart() instead
1372 */
1373 resume: function() {
1374 this.restart();
1375 },
1376
1377 /**
1378 * Restarts the interval-based callback execution.
1379 */
1380 restart: function() {
1381 if (this._intervalID) {
1382 this.stop();
1383 }
1384
1385 this._intervalID = setInterval($.proxy(this._execute, this), this._interval);
1386 },
1387
1388 /**
1389 * Sets the interval and restarts the interval.
1390 *
1391 * @param integer interval
1392 */
1393 setInterval: function(interval) {
1394 this._interval = interval;
1395
1396 this.restart();
1397 }
1398 });
1399
1400 /**
1401 * Handler for loading overlays
1402 *
1403 * @deprecated 2.2 - Please use WoltLab/WCF/Ajax/Status
1404 */
1405 WCF.LoadingOverlayHandler = {
1406 /**
1407 * Adds one loading-request and shows the loading overlay if nessercery
1408 */
1409 show: function() {
1410 require(['WoltLab/WCF/Ajax/Status'], function(AjaxStatus) {
1411 AjaxStatus.show();
1412 });
1413 },
1414
1415 /**
1416 * Removes one loading-request and hides loading overlay if there're no more pending requests
1417 */
1418 hide: function() {
1419 require(['WoltLab/WCF/Ajax/Status'], function(AjaxStatus) {
1420 AjaxStatus.hide();
1421 });
1422 },
1423
1424 /**
1425 * Updates a icon to/from spinner
1426 *
1427 * @param jQuery target
1428 * @pram boolean loading
1429 */
1430 updateIcon: function(target, loading) {
1431 var $method = (loading === undefined || loading ? 'addClass' : 'removeClass');
1432
1433 target.find('.icon')[$method]('fa-spinner');
1434 if (target.hasClass('icon')) {
1435 target[$method]('fa-spinner');
1436 }
1437 }
1438 };
1439
1440 /**
1441 * Namespace for AJAXProxies
1442 */
1443 WCF.Action = {};
1444
1445 /**
1446 * Basic implementation for AJAX-based proxyies
1447 *
1448 * @deprecated 2.2 - please use `WoltLab/WCF/Ajax.api()` instead
1449 *
1450 * @param object options
1451 */
1452 WCF.Action.Proxy = Class.extend({
1453 _ajaxRequest: null,
1454
1455 /**
1456 * Initializes AJAXProxy.
1457 *
1458 * @param object options
1459 */
1460 init: function(options) {
1461 this._ajaxRequest = null;
1462
1463 options = $.extend(true, {
1464 autoSend: false,
1465 data: { },
1466 dataType: 'json',
1467 after: null,
1468 init: null,
1469 jsonp: 'callback',
1470 async: true,
1471 failure: null,
1472 showLoadingOverlay: true,
1473 success: null,
1474 suppressErrors: false,
1475 type: 'POST',
1476 url: 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND,
1477 aborted: null,
1478 autoAbortPrevious: false
1479 }, options);
1480
1481 if (options.dataType === 'jsonp') {
1482 require(['AjaxJsonp'], function(AjaxJsonp) {
1483 AjaxJsonp.send(options.url, options.success, options.failure, {
1484 parameterName: options.jsonp
1485 });
1486 });
1487 }
1488 else {
1489 require(['AjaxRequest'], (function(AjaxRequest) {
1490 this._ajaxRequest = new AjaxRequest({
1491 data: options.data,
1492 type: options.type,
1493 url: options.url,
1494
1495 autoAbort: options.autoAbortPrevious,
1496 ignoreError: options.suppressErrors,
1497 silent: !options.showLoadingOverlay,
1498
1499 failure: options.failure,
1500 finalize: options.after,
1501 success: options.success
1502 });
1503
1504 if (options.autoSend) {
1505 this._ajaxRequest.sendRequest();
1506 }
1507 }).bind(this));
1508 }
1509 },
1510
1511 /**
1512 * Sends an AJAX request.
1513 *
1514 * @param abortPrevious boolean
1515 */
1516 sendRequest: function(abortPrevious) {
1517 require(['AjaxRequest'], (function(AjaxRequest) {
1518 if (this._ajaxRequest !== null) {
1519 this._ajaxRequest.sendRequest(abortPrevious);
1520 }
1521 }).bind(this));
1522 },
1523
1524 /**
1525 * Aborts the previous request
1526 */
1527 abortPrevious: function() {
1528 require(['AjaxRequest'], (function(AjaxRequest) {
1529 if (this._ajaxRequest !== null) {
1530 this._ajaxRequest.abortPrevious();
1531 }
1532 }).bind(this));
1533 },
1534
1535 /**
1536 * Sets options, MUST be used to set parameters before sending request
1537 * if calling from child classes.
1538 *
1539 * @param string optionName
1540 * @param mixed optionData
1541 */
1542 setOption: function(optionName, optionData) {
1543 require(['AjaxRequest'], (function(AjaxRequest) {
1544 if (this._ajaxRequest !== null) {
1545 this._ajaxRequest.setOption(optionName, optionData);
1546 }
1547 }).bind(this));
1548 },
1549
1550 // legacy methods, no longer supported
1551 showLoadingOverlayOnce: function() {},
1552 suppressErrors: function() {},
1553 _failure: function(jqXHR, textStatus, errorThrown) {},
1554 _success: function(data, textStatus, jqXHR) {},
1555 _after: function() {}
1556 });
1557
1558 /**
1559 * Basic implementation for simple proxy access using bound elements.
1560 *
1561 * @param object options
1562 * @param object callbacks
1563 */
1564 WCF.Action.SimpleProxy = Class.extend({
1565 /**
1566 * Initializes SimpleProxy.
1567 *
1568 * @param object options
1569 * @param object callbacks
1570 */
1571 init: function(options, callbacks) {
1572 /**
1573 * action-specific options
1574 */
1575 this.options = $.extend(true, {
1576 action: '',
1577 className: '',
1578 elements: null,
1579 eventName: 'click'
1580 }, options);
1581
1582 /**
1583 * proxy-specific options
1584 */
1585 this.callbacks = $.extend(true, {
1586 after: null,
1587 failure: null,
1588 init: null,
1589 success: null
1590 }, callbacks);
1591
1592 if (!this.options.elements) return;
1593
1594 // initialize proxy
1595 this.proxy = new WCF.Action.Proxy(this.callbacks);
1596
1597 // bind event listener
1598 this.options.elements.each($.proxy(function(index, element) {
1599 $(element).bind(this.options.eventName, $.proxy(this._handleEvent, this));
1600 }, this));
1601 },
1602
1603 /**
1604 * Handles event actions.
1605 *
1606 * @param object event
1607 */
1608 _handleEvent: function(event) {
1609 this.proxy.setOption('data', {
1610 actionName: this.options.action,
1611 className: this.options.className,
1612 objectIDs: [ $(event.target).data('objectID') ]
1613 });
1614
1615 this.proxy.sendRequest();
1616 }
1617 });
1618
1619 /**
1620 * Basic implementation for AJAXProxy-based deletion.
1621 *
1622 * @param string className
1623 * @param string containerSelector
1624 * @param string buttonSelector
1625 */
1626 WCF.Action.Delete = Class.extend({
1627 /**
1628 * delete button selector
1629 * @var string
1630 */
1631 _buttonSelector: '',
1632
1633 /**
1634 * action class name
1635 * @var string
1636 */
1637 _className: '',
1638
1639 /**
1640 * container selector
1641 * @var string
1642 */
1643 _containerSelector: '',
1644
1645 /**
1646 * list of known container ids
1647 * @var array<string>
1648 */
1649 _containers: [ ],
1650
1651 /**
1652 * Initializes 'delete'-Proxy.
1653 *
1654 * @param string className
1655 * @param string containerSelector
1656 * @param string buttonSelector
1657 */
1658 init: function(className, containerSelector, buttonSelector) {
1659 this._containerSelector = containerSelector;
1660 this._className = className;
1661 this._buttonSelector = (buttonSelector) ? buttonSelector : '.jsDeleteButton';
1662
1663 this.proxy = new WCF.Action.Proxy({
1664 success: $.proxy(this._success, this)
1665 });
1666
1667 this._initElements();
1668
1669 WCF.DOMNodeInsertedHandler.addCallback('WCF.Action.Delete' + this._className.hashCode(), $.proxy(this._initElements, this));
1670 },
1671
1672 /**
1673 * Initializes available element containers.
1674 */
1675 _initElements: function() {
1676 $(this._containerSelector).each((function(index, container) {
1677 var $container = $(container);
1678 var $containerID = $container.wcfIdentify();
1679
1680 if (!WCF.inArray($containerID, this._containers)) {
1681 var $deleteButton = $container.find(this._buttonSelector);
1682
1683 if ($deleteButton.length) {
1684 this._containers.push($containerID);
1685 $deleteButton.click($.proxy(this._click, this));
1686 }
1687 }
1688 }).bind(this));
1689 },
1690
1691 /**
1692 * Sends AJAX request.
1693 *
1694 * @param object event
1695 */
1696 _click: function(event) {
1697 var $target = $(event.currentTarget);
1698 event.preventDefault();
1699
1700 if ($target.data('confirmMessage')) {
1701 WCF.System.Confirmation.show($target.data('confirmMessage'), $.proxy(this._execute, this), { target: $target });
1702 }
1703 else {
1704 WCF.LoadingOverlayHandler.updateIcon($target);
1705 this._sendRequest($target);
1706 }
1707 },
1708
1709 /**
1710 * Is called if the delete effect has been triggered on the given element.
1711 *
1712 * @param jQuery element
1713 */
1714 _didTriggerEffect: function(element) {
1715 // does nothing
1716 },
1717
1718 /**
1719 * Executes deletion.
1720 *
1721 * @param string action
1722 * @param object parameters
1723 */
1724 _execute: function(action, parameters) {
1725 if (action === 'cancel') {
1726 return;
1727 }
1728
1729 WCF.LoadingOverlayHandler.updateIcon(parameters.target);
1730 this._sendRequest(parameters.target);
1731 },
1732
1733 /**
1734 * Sends the request
1735 *
1736 * @param jQuery object
1737 */
1738 _sendRequest: function(object) {
1739 this.proxy.setOption('data', {
1740 actionName: 'delete',
1741 className: this._className,
1742 interfaceName: 'wcf\\data\\IDeleteAction',
1743 objectIDs: [ $(object).data('objectID') ]
1744 });
1745
1746 this.proxy.sendRequest();
1747 },
1748
1749 /**
1750 * Deletes items from containers.
1751 *
1752 * @param object data
1753 * @param string textStatus
1754 * @param object jqXHR
1755 */
1756 _success: function(data, textStatus, jqXHR) {
1757 this.triggerEffect(data.objectIDs);
1758 },
1759
1760 /**
1761 * Triggers the delete effect for the objects with the given ids.
1762 *
1763 * @param array objectIDs
1764 */
1765 triggerEffect: function(objectIDs) {
1766 for (var $index in this._containers) {
1767 var $container = $('#' + this._containers[$index]);
1768 var $button = $container.find(this._buttonSelector);
1769 if (WCF.inArray($button.data('objectID'), objectIDs)) {
1770 var self = this;
1771 $container.wcfBlindOut('up',function() {
1772 var $container = $(this).remove();
1773 self._containers.splice(self._containers.indexOf($container.wcfIdentify()), 1);
1774 self._didTriggerEffect($container);
1775
1776 if ($button.data('eventName')) {
1777 WCF.System.Event.fireEvent('com.woltlab.wcf.action.delete', $button.data('eventName'), {
1778 button: $button,
1779 container: $container
1780 });
1781 }
1782 });
1783 }
1784 }
1785 }
1786 });
1787
1788 /**
1789 * Basic implementation for deletion of nested elements.
1790 *
1791 * The implementation requires the nested elements to be grouped as numbered lists
1792 * (ol lists). The child elements of the deleted elements are moved to the parent
1793 * element of the deleted element.
1794 *
1795 * @see WCF.Action.Delete
1796 */
1797 WCF.Action.NestedDelete = WCF.Action.Delete.extend({
1798 /**
1799 * @see WCF.Action.Delete.triggerEffect()
1800 */
1801 triggerEffect: function(objectIDs) {
1802 for (var $index in this._containers) {
1803 var $container = $('#' + this._containers[$index]);
1804 if (WCF.inArray($container.find(this._buttonSelector).data('objectID'), objectIDs)) {
1805 // move children up
1806 if ($container.has('ol').has('li').length) {
1807 if ($container.is(':only-child')) {
1808 $container.parent().replaceWith($container.find('> ol'));
1809 }
1810 else {
1811 $container.replaceWith($container.find('> ol > li'));
1812 }
1813
1814 this._containers.splice(this._containers.indexOf($container.wcfIdentify()), 1);
1815 this._didTriggerEffect($container);
1816 }
1817 else {
1818 var self = this;
1819 $container.wcfBlindOut('up', function() {
1820 $(this).remove();
1821 self._containers.splice(self._containers.indexOf($(this).wcfIdentify()), 1);
1822 self._didTriggerEffect($(this));
1823 });
1824 }
1825 }
1826 }
1827 }
1828 });
1829
1830 /**
1831 * Basic implementation for AJAXProxy-based toggle actions.
1832 *
1833 * @param string className
1834 * @param jQuery containerList
1835 * @param string buttonSelector
1836 */
1837 WCF.Action.Toggle = Class.extend({
1838 /**
1839 * toogle button selector
1840 * @var string
1841 */
1842 _buttonSelector: '.jsToggleButton',
1843
1844 /**
1845 * action class name
1846 * @var string
1847 */
1848 _className: '',
1849
1850 /**
1851 * container selector
1852 * @var string
1853 */
1854 _containerSelector: '',
1855
1856 /**
1857 * list of known container ids
1858 * @var array<string>
1859 */
1860 _containers: [ ],
1861
1862 /**
1863 * Initializes 'toggle'-Proxy
1864 *
1865 * @param string className
1866 * @param string containerSelector
1867 * @param string buttonSelector
1868 */
1869 init: function(className, containerSelector, buttonSelector) {
1870 this._containerSelector = containerSelector;
1871 this._className = className;
1872 this._buttonSelector = (buttonSelector) ? buttonSelector : '.jsToggleButton';
1873 this._containers = [ ];
1874
1875 // initialize proxy
1876 var options = {
1877 success: $.proxy(this._success, this)
1878 };
1879 this.proxy = new WCF.Action.Proxy(options);
1880
1881 // bind event listener
1882 this._initElements();
1883 WCF.DOMNodeInsertedHandler.addCallback('WCF.Action.Toggle' + this._className.hashCode(), $.proxy(this._initElements, this));
1884 },
1885
1886 /**
1887 * Initializes available element containers.
1888 */
1889 _initElements: function() {
1890 $(this._containerSelector).each($.proxy(function(index, container) {
1891 var $container = $(container);
1892 var $containerID = $container.wcfIdentify();
1893
1894 if (!WCF.inArray($containerID, this._containers)) {
1895 this._containers.push($containerID);
1896 $container.find(this._buttonSelector).click($.proxy(this._click, this));
1897 }
1898 }, this));
1899 },
1900
1901 /**
1902 * Sends AJAX request.
1903 *
1904 * @param object event
1905 */
1906 _click: function(event) {
1907 var $target = $(event.currentTarget);
1908 event.preventDefault();
1909
1910 if ($target.data('confirmMessage')) {
1911 WCF.System.Confirmation.show($target.data('confirmMessage'), $.proxy(this._execute, this), { target: $target });
1912 }
1913 else {
1914 WCF.LoadingOverlayHandler.updateIcon($target);
1915 this._sendRequest($target);
1916 }
1917 },
1918
1919 /**
1920 * Executes toggeling.
1921 *
1922 * @param string action
1923 * @param object parameters
1924 */
1925 _execute: function(action, parameters) {
1926 if (action === 'cancel') {
1927 return;
1928 }
1929
1930 WCF.LoadingOverlayHandler.updateIcon(parameters.target);
1931 this._sendRequest(parameters.target);
1932 },
1933
1934 _sendRequest: function(object) {
1935 this.proxy.setOption('data', {
1936 actionName: 'toggle',
1937 className: this._className,
1938 interfaceName: 'wcf\\data\\IToggleAction',
1939 objectIDs: [ $(object).data('objectID') ]
1940 });
1941
1942 this.proxy.sendRequest();
1943 },
1944
1945 /**
1946 * Toggles status icons.
1947 *
1948 * @param object data
1949 * @param string textStatus
1950 * @param object jqXHR
1951 */
1952 _success: function(data, textStatus, jqXHR) {
1953 this.triggerEffect(data.objectIDs);
1954 },
1955
1956 /**
1957 * Triggers the toggle effect for the objects with the given ids.
1958 *
1959 * @param array objectIDs
1960 */
1961 triggerEffect: function(objectIDs) {
1962 for (var $index in this._containers) {
1963 var $container = $('#' + this._containers[$index]);
1964 var $toggleButton = $container.find(this._buttonSelector);
1965 if (WCF.inArray($toggleButton.data('objectID'), objectIDs)) {
1966 $container.wcfHighlight();
1967 this._toggleButton($container, $toggleButton);
1968 }
1969 }
1970 },
1971
1972 /**
1973 * Tiggers the toggle effect on a button
1974 *
1975 * @param jQuery $container
1976 * @param jQuery $toggleButton
1977 */
1978 _toggleButton: function($container, $toggleButton) {
1979 var $newTitle = '';
1980
1981 // toggle icon source
1982 WCF.LoadingOverlayHandler.updateIcon($toggleButton, false);
1983 if ($toggleButton.hasClass('fa-square-o')) {
1984 $toggleButton.removeClass('fa-square-o').addClass('fa-check-square-o');
1985 $newTitle = ($toggleButton.data('disableTitle') ? $toggleButton.data('disableTitle') : WCF.Language.get('wcf.global.button.disable'));
1986 $toggleButton.attr('title', $newTitle);
1987 }
1988 else {
1989 $toggleButton.removeClass('fa-check-square-o').addClass('fa-square-o');
1990 $newTitle = ($toggleButton.data('enableTitle') ? $toggleButton.data('enableTitle') : WCF.Language.get('wcf.global.button.enable'));
1991 $toggleButton.attr('title', $newTitle);
1992 }
1993
1994 // toggle css class
1995 $container.toggleClass('disabled');
1996 }
1997 });
1998
1999 /**
2000 * Executes provided callback if scroll threshold is reached. Usuable to determine
2001 * if user reached the bottom of an element to load new elements on the fly.
2002 *
2003 * If you do not provide a value for 'reference' and 'target' it will assume you're
2004 * monitoring page scrolls, otherwise a valid jQuery selector must be provided for both.
2005 *
2006 * @param integer threshold
2007 * @param object callback
2008 * @param string reference
2009 * @param string target
2010 */
2011 WCF.Action.Scroll = Class.extend({
2012 /**
2013 * callback used once threshold is reached
2014 * @var object
2015 */
2016 _callback: null,
2017
2018 /**
2019 * reference object
2020 * @var jQuery
2021 */
2022 _reference: null,
2023
2024 /**
2025 * target object
2026 * @var jQuery
2027 */
2028 _target: null,
2029
2030 /**
2031 * threshold value
2032 * @var integer
2033 */
2034 _threshold: 0,
2035
2036 /**
2037 * Initializes a new WCF.Action.Scroll object.
2038 *
2039 * @param integer threshold
2040 * @param object callback
2041 * @param string reference
2042 * @param string target
2043 */
2044 init: function(threshold, callback, reference, target) {
2045 this._threshold = parseInt(threshold);
2046 if (this._threshold === 0) {
2047 console.debug("[WCF.Action.Scroll] Given threshold is invalid, aborting.");
2048 return;
2049 }
2050
2051 if ($.isFunction(callback)) this._callback = callback;
2052 if (this._callback === null) {
2053 console.debug("[WCF.Action.Scroll] Given callback is invalid, aborting.");
2054 return;
2055 }
2056
2057 // bind element references
2058 this._reference = $((reference) ? reference : window);
2059 this._target = $((target) ? target : document);
2060
2061 // watch for scroll event
2062 this.start();
2063
2064 // check if browser navigated back and jumped to offset before JavaScript was loaded
2065 this._scroll();
2066 },
2067
2068 /**
2069 * Calculates if threshold is reached and notifies callback.
2070 */
2071 _scroll: function() {
2072 var $targetHeight = this._target.height();
2073 var $topOffset = this._reference.scrollTop();
2074 var $referenceHeight = this._reference.height();
2075
2076 // calculate if defined threshold is visible
2077 if (($targetHeight - ($referenceHeight + $topOffset)) < this._threshold) {
2078 this._callback(this);
2079 }
2080 },
2081
2082 /**
2083 * Enables scroll monitoring, may be used to resume.
2084 */
2085 start: function() {
2086 this._reference.on('scroll', $.proxy(this._scroll, this));
2087 },
2088
2089 /**
2090 * Disables scroll monitoring, e.g. no more elements loadable.
2091 */
2092 stop: function() {
2093 this._reference.off('scroll');
2094 }
2095 });
2096
2097 /**
2098 * Namespace for date-related functions.
2099 */
2100 WCF.Date = {};
2101
2102 /**
2103 * Provides a date picker for date input fields.
2104 *
2105 * @deprecated 2.2 - no longer required
2106 */
2107 WCF.Date.Picker = { init: function() {} };
2108
2109 /**
2110 * Provides utility functions for date operations.
2111 *
2112 * @deprecated 2.2 - use `DateUtil` instead
2113 */
2114 WCF.Date.Util = {
2115 /**
2116 * Returns UTC timestamp, if date is not given, current time will be used.
2117 *
2118 * @param Date date
2119 * @return integer
2120 *
2121 * @deprecated 2.2 - use `DateUtil::gmdate()` instead
2122 */
2123 gmdate: function(date) {
2124 var $date = (date) ? date : new Date();
2125
2126 return Math.round(Date.UTC(
2127 $date.getUTCFullYear(),
2128 $date.getUTCMonth(),
2129 $date.getUTCDay(),
2130 $date.getUTCHours(),
2131 $date.getUTCMinutes(),
2132 $date.getUTCSeconds()
2133 ) / 1000);
2134 },
2135
2136 /**
2137 * Returns a Date object with precise offset (including timezone and local timezone).
2138 * Parameters timestamp and offset must be in miliseconds!
2139 *
2140 * @param integer timestamp
2141 * @param integer offset
2142 * @return Date
2143 *
2144 * @deprecated 2.2 - use `DateUtil::getTimezoneDate()` instead
2145 */
2146 getTimezoneDate: function(timestamp, offset) {
2147 var $date = new Date(timestamp);
2148 var $localOffset = $date.getTimezoneOffset() * 60000;
2149
2150 return new Date((timestamp + $localOffset + offset));
2151 }
2152 };
2153
2154 /**
2155 * Hash-like dictionary. Based upon idead from Prototype's hash
2156 *
2157 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/hash.js
2158 */
2159 WCF.Dictionary = Class.extend({
2160 /**
2161 * list of variables
2162 * @var object
2163 */
2164 _variables: { },
2165
2166 /**
2167 * Initializes a new dictionary.
2168 */
2169 init: function() {
2170 this._variables = { };
2171 },
2172
2173 /**
2174 * Adds an entry.
2175 *
2176 * @param string key
2177 * @param mixed value
2178 */
2179 add: function(key, value) {
2180 this._variables[key] = value;
2181 },
2182
2183 /**
2184 * Adds a traditional object to current dataset.
2185 *
2186 * @param object object
2187 */
2188 addObject: function(object) {
2189 for (var $key in object) {
2190 this.add($key, object[$key]);
2191 }
2192 },
2193
2194 /**
2195 * Adds a dictionary to current dataset.
2196 *
2197 * @param object dictionary
2198 */
2199 addDictionary: function(dictionary) {
2200 dictionary.each($.proxy(function(pair) {
2201 this.add(pair.key, pair.value);
2202 }, this));
2203 },
2204
2205 /**
2206 * Retrieves the value of an entry or returns null if key is not found.
2207 *
2208 * @param string key
2209 * @returns mixed
2210 */
2211 get: function(key) {
2212 if (this.isset(key)) {
2213 return this._variables[key];
2214 }
2215
2216 return null;
2217 },
2218
2219 /**
2220 * Returns true if given key is a valid entry.
2221 *
2222 * @param string key
2223 */
2224 isset: function(key) {
2225 return this._variables.hasOwnProperty(key);
2226 },
2227
2228 /**
2229 * Removes an entry.
2230 *
2231 * @param string key
2232 */
2233 remove: function(key) {
2234 delete this._variables[key];
2235 },
2236
2237 /**
2238 * Iterates through dictionary.
2239 *
2240 * Usage:
2241 * var $hash = new WCF.Dictionary();
2242 * $hash.add('foo', 'bar');
2243 * $hash.each(function(pair) {
2244 * // alerts: foo = bar
2245 * alert(pair.key + ' = ' + pair.value);
2246 * });
2247 *
2248 * @param function callback
2249 */
2250 each: function(callback) {
2251 if (!$.isFunction(callback)) {
2252 return;
2253 }
2254
2255 for (var $key in this._variables) {
2256 var $value = this._variables[$key];
2257 var $pair = {
2258 key: $key,
2259 value: $value
2260 };
2261
2262 callback($pair);
2263 }
2264 },
2265
2266 /**
2267 * Returns the amount of items.
2268 *
2269 * @return integer
2270 */
2271 count: function() {
2272 return $.getLength(this._variables);
2273 },
2274
2275 /**
2276 * Returns true if dictionary is empty.
2277 *
2278 * @return integer
2279 */
2280 isEmpty: function() {
2281 return !this.count();
2282 }
2283 });
2284
2285 // non strict equals by intent
2286 if (window.WCF.Language == null) {
2287 /**
2288 * @deprecated Use WoltLab/WCF/Language
2289 */
2290 WCF.Language = {
2291 add: function(key, value) {
2292 require(['WoltLab/WCF/Language'], function(Language) {
2293 Language.add(key, value);
2294 });
2295 },
2296 addObject: function(object) {
2297 require(['WoltLab/WCF/Language'], function(Language) {
2298 Language.addObject(object);
2299 });
2300 },
2301 get: function(key, parameters) {
2302 // This cannot be sanely provided as a compatibility wrapper.
2303 throw new Error('Call to deprecated WCF.Language.get("' + key + '")');
2304 }
2305 };
2306 }
2307
2308 /**
2309 * Handles multiple language input fields.
2310 *
2311 * @param string elementID
2312 * @param boolean forceSelection
2313 * @param object values
2314 * @param object availableLanguages
2315 */
2316 WCF.MultipleLanguageInput = Class.extend({
2317 /**
2318 * list of available languages
2319 * @var object
2320 */
2321 _availableLanguages: {},
2322
2323 /**
2324 * button element
2325 * @var jQuery
2326 */
2327 _button: null,
2328
2329 /**
2330 * initialization state
2331 * @var boolean
2332 */
2333 _didInit: false,
2334
2335 /**
2336 * target input element
2337 * @var jQuery
2338 */
2339 _element: null,
2340
2341 /**
2342 * true, if data was entered after initialization
2343 * @var boolean
2344 */
2345 _insertedDataAfterInit: false,
2346
2347 /**
2348 * enables multiple language ability
2349 * @var boolean
2350 */
2351 _isEnabled: false,
2352
2353 /**
2354 * enforce multiple language ability
2355 * @var boolean
2356 */
2357 _forceSelection: false,
2358
2359 /**
2360 * currently active language id
2361 * @var integer
2362 */
2363 _languageID: 0,
2364
2365 /**
2366 * language selection list
2367 * @var jQuery
2368 */
2369 _list: null,
2370
2371 /**
2372 * list of language values on init
2373 * @var object
2374 */
2375 _values: null,
2376
2377 /**
2378 * Initializes multiple language ability for given element id.
2379 *
2380 * @param integer elementID
2381 * @param boolean forceSelection
2382 * @param boolean isEnabled
2383 * @param object values
2384 * @param object availableLanguages
2385 */
2386 init: function(elementID, forceSelection, values, availableLanguages) {
2387 this._button = null;
2388 this._element = $('#' + $.wcfEscapeID(elementID));
2389 this._forceSelection = forceSelection;
2390 this._values = values;
2391 this._availableLanguages = availableLanguages;
2392
2393 // unescape values
2394 if ($.getLength(this._values)) {
2395 for (var $key in this._values) {
2396 this._values[$key] = WCF.String.unescapeHTML(this._values[$key]);
2397 }
2398 }
2399
2400 // default to current user language
2401 this._languageID = LANGUAGE_ID;
2402 if (this._element.length == 0) {
2403 console.debug("[WCF.MultipleLanguageInput] element id '" + elementID + "' is unknown");
2404 return;
2405 }
2406
2407 // build selection handler
2408 var $enableOnInit = ($.getLength(this._values) > 0) ? true : false;
2409 this._insertedDataAfterInit = $enableOnInit;
2410 this._prepareElement($enableOnInit);
2411
2412 // listen for submit event
2413 this._element.parents('form').submit($.proxy(this._submit, this));
2414
2415 this._didInit = true;
2416 },
2417
2418 /**
2419 * Builds language handler.
2420 *
2421 * @param boolean enableOnInit
2422 */
2423 _prepareElement: function(enableOnInit) {
2424 this._element.wrap('<div class="dropdown preInput" />');
2425 var $wrapper = this._element.parent();
2426 this._button = $('<p class="button dropdownToggle"><span>' + WCF.Language.get('wcf.global.button.disabledI18n') + '</span></p>').prependTo($wrapper);
2427
2428 // insert list
2429 this._list = $('<ul class="dropdownMenu"></ul>').insertAfter(this._button);
2430
2431 // add a special class if next item is a textarea
2432 if (this._button.nextAll('textarea').length) {
2433 this._button.addClass('dropdownCaptionTextarea');
2434 }
2435 else {
2436 this._button.addClass('dropdownCaption');
2437 }
2438
2439 // insert available languages
2440 for (var $languageID in this._availableLanguages) {
2441 $('<li><span>' + this._availableLanguages[$languageID] + '</span></li>').data('languageID', $languageID).click($.proxy(this._changeLanguage, this)).appendTo(this._list);
2442 }
2443
2444 // disable language input
2445 if (!this._forceSelection) {
2446 $('<li class="dropdownDivider" />').appendTo(this._list);
2447 $('<li><span>' + WCF.Language.get('wcf.global.button.disabledI18n') + '</span></li>').click($.proxy(this._disable, this)).appendTo(this._list);
2448 }
2449
2450 WCF.Dropdown.initDropdown(this._button, enableOnInit);
2451
2452 if (enableOnInit || this._forceSelection) {
2453 this._isEnabled = true;
2454
2455 // pre-select current language
2456 this._list.children('li').each($.proxy(function(index, listItem) {
2457 var $listItem = $(listItem);
2458 if ($listItem.data('languageID') == this._languageID) {
2459 $listItem.trigger('click');
2460 }
2461 }, this));
2462 }
2463
2464 WCF.Dropdown.registerCallback($wrapper.wcfIdentify(), $.proxy(this._handleAction, this));
2465 },
2466
2467 /**
2468 * Handles dropdown actions.
2469 *
2470 * @param string containerID
2471 * @param string action
2472 */
2473 _handleAction: function(containerID, action) {
2474 if (action === 'open') {
2475 this._enable();
2476 }
2477 else {
2478 this._closeSelection();
2479 }
2480 },
2481
2482 /**
2483 * Enables the language selection or shows the selection if already enabled.
2484 *
2485 * @param object event
2486 */
2487 _enable: function(event) {
2488 if (!this._isEnabled) {
2489 var $button = (this._button.is('p')) ? this._button.children('span:eq(0)') : this._button;
2490 $button.addClass('active');
2491
2492 this._isEnabled = true;
2493 }
2494
2495 // toggle list
2496 if (this._list.is(':visible')) {
2497 this._showSelection();
2498 }
2499 },
2500
2501 /**
2502 * Shows the language selection.
2503 */
2504 _showSelection: function() {
2505 if (this._isEnabled) {
2506 // display status for each language
2507 this._list.children('li').each($.proxy(function(index, listItem) {
2508 var $listItem = $(listItem);
2509 var $languageID = $listItem.data('languageID');
2510
2511 if ($languageID) {
2512 if (this._values[$languageID] && this._values[$languageID] != '') {
2513 $listItem.removeClass('missingValue');
2514 }
2515 else {
2516 $listItem.addClass('missingValue');
2517 }
2518 }
2519 }, this));
2520 }
2521 },
2522
2523 /**
2524 * Closes the language selection.
2525 */
2526 _closeSelection: function() {
2527 this._disable();
2528 },
2529
2530 /**
2531 * Changes the currently active language.
2532 *
2533 * @param object event
2534 */
2535 _changeLanguage: function(event) {
2536 var $button = $(event.currentTarget);
2537 this._insertedDataAfterInit = true;
2538
2539 // save current value
2540 if (this._didInit) {
2541 this._values[this._languageID] = this._element.val();
2542 }
2543
2544 // set new language
2545 this._languageID = $button.data('languageID');
2546 if (this._values[this._languageID]) {
2547 this._element.val(this._values[this._languageID]);
2548 }
2549 else {
2550 this._element.val('');
2551 }
2552
2553 // update marking
2554 this._list.children('li').removeClass('active');
2555 $button.addClass('active');
2556
2557 // update label
2558 this._button.children('span').addClass('active').text(this._availableLanguages[this._languageID]);
2559
2560 // close selection and set focus on input element
2561 if (this._didInit) {
2562 this._element.blur().focus();
2563 }
2564 },
2565
2566 /**
2567 * Disables language selection for current element.
2568 *
2569 * @param object event
2570 */
2571 _disable: function(event) {
2572 if (event === undefined && this._insertedDataAfterInit) {
2573 event = null;
2574 }
2575
2576 if (this._forceSelection || !this._list || event === null) {
2577 return;
2578 }
2579
2580 // remove active marking
2581 this._button.children('span').removeClass('active').text(WCF.Language.get('wcf.global.button.disabledI18n'));
2582
2583 // update element value
2584 if (this._values[LANGUAGE_ID]) {
2585 this._element.val(this._values[LANGUAGE_ID]);
2586 }
2587 else {
2588 // no value for current language found, proceed with empty input
2589 this._element.val();
2590 }
2591
2592 if (event) {
2593 this._list.children('li').removeClass('active');
2594 $(event.currentTarget).addClass('active');
2595 }
2596
2597 this._element.blur().focus();
2598 this._insertedDataAfterInit = false;
2599 this._isEnabled = false;
2600 this._values = { };
2601 },
2602
2603 /**
2604 * Prepares language variables on before submit.
2605 */
2606 _submit: function() {
2607 // insert hidden form elements on before submit
2608 if (!this._isEnabled) {
2609 return 0xDEADBEEF;
2610 }
2611
2612 // fetch active value
2613 if (this._languageID) {
2614 this._values[this._languageID] = this._element.val();
2615 }
2616
2617 var $form = $(this._element.parents('form')[0]);
2618 var $elementID = this._element.wcfIdentify();
2619
2620 for (var $languageID in this._availableLanguages) {
2621 if (this._values[$languageID] === undefined) {
2622 this._values[$languageID] = '';
2623 }
2624
2625 $('<input type="hidden" name="' + $elementID + '_i18n[' + $languageID + ']" value="' + WCF.String.escapeHTML(this._values[$languageID]) + '" />').appendTo($form);
2626 }
2627
2628 // remove name attribute to prevent conflict with i18n values
2629 this._element.removeAttr('name');
2630 }
2631 });
2632
2633 /**
2634 * Number utilities.
2635 * @deprecated Use WoltLab/WCF/NumberUtil
2636 */
2637 WCF.Number = {
2638 /**
2639 * Rounds a number to a given number of decimal places. Defaults to 0.
2640 *
2641 * @param number number
2642 * @param decimalPlaces number of decimal places
2643 * @return number
2644 */
2645 round: function (number, decimalPlaces) {
2646 decimalPlaces = Math.pow(10, (decimalPlaces || 0));
2647
2648 return Math.round(number * decimalPlaces) / decimalPlaces;
2649 }
2650 };
2651
2652 /**
2653 * String utilities.
2654 * @deprecated Use WoltLab/WCF/StringUtil
2655 */
2656 WCF.String = {
2657 /**
2658 * Adds thousands separators to a given number.
2659 *
2660 * @see http://stackoverflow.com/a/6502556/782822
2661 * @param mixed number
2662 * @return string
2663 */
2664 addThousandsSeparator: function(number) {
2665 return String(number).replace(/(^-?\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, '$1' + WCF.Language.get('wcf.global.thousandsSeparator'));
2666 },
2667
2668 /**
2669 * Escapes special HTML-characters within a string
2670 *
2671 * @param string string
2672 * @return string
2673 */
2674 escapeHTML: function (string) {
2675 return String(string).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2676 },
2677
2678 /**
2679 * Escapes a String to work with RegExp.
2680 *
2681 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25
2682 * @param string string
2683 * @return string
2684 */
2685 escapeRegExp: function(string) {
2686 return String(string).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
2687 },
2688
2689 /**
2690 * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands-separators
2691 *
2692 * @param mixed number
2693 * @return string
2694 */
2695 formatNumeric: function(number, decimalPlaces) {
2696 number = String(WCF.Number.round(number, decimalPlaces || 2));
2697 var numberParts = number.split('.');
2698
2699 number = this.addThousandsSeparator(numberParts[0]);
2700 if (numberParts.length > 1) number += WCF.Language.get('wcf.global.decimalPoint') + numberParts[1];
2701
2702 number = number.replace('-', '\u2212');
2703
2704 return number;
2705 },
2706
2707 /**
2708 * Makes a string's first character lowercase
2709 *
2710 * @param string string
2711 * @return string
2712 */
2713 lcfirst: function(string) {
2714 return String(string).substring(0, 1).toLowerCase() + string.substring(1);
2715 },
2716
2717 /**
2718 * Makes a string's first character uppercase
2719 *
2720 * @param string string
2721 * @return string
2722 */
2723 ucfirst: function(string) {
2724 return String(string).substring(0, 1).toUpperCase() + string.substring(1);
2725 },
2726
2727 /**
2728 * Unescapes special HTML-characters within a string
2729 *
2730 * @param string string
2731 * @return string
2732 */
2733 unescapeHTML: function (string) {
2734 return String(string).replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
2735 }
2736 };
2737
2738 /**
2739 * Basic implementation for WCF TabMenus. Use the data attributes 'active' to specify the
2740 * tab which should be shown on init. Furthermore you may specify a 'store' data-attribute
2741 * which will be filled with the currently selected tab.
2742 */
2743 WCF.TabMenu = {
2744 /**
2745 * list of tabmenu containers
2746 * @var object
2747 */
2748 _containers: { },
2749
2750 /**
2751 * initialization state
2752 * @var boolean
2753 */
2754 _didInit: false,
2755
2756 /**
2757 * Initializes all TabMenus
2758 */
2759 init: function() {
2760 require(['WoltLab/WCF/Ui/TabMenu'], function(UiTabMenu) {
2761 UiTabMenu.setup();
2762 });
2763
2764 return;
2765
2766 var $containers = $('.tabMenuContainer:not(.staticTabMenuContainer)');
2767 var self = this;
2768 $containers.each(function(index, tabMenu) {
2769 var $tabMenu = $(tabMenu);
2770 var $containerID = $tabMenu.wcfIdentify();
2771 if (self._containers[$containerID]) {
2772 // continue with next container
2773 return true;
2774 }
2775
2776 if ($tabMenu.data('store') && !$('#' + $tabMenu.data('store')).length) {
2777 $('<input type="hidden" name="' + $tabMenu.data('store') + '" value="" id="' + $tabMenu.data('store') + '" />').appendTo($tabMenu.parents('form').find('.formSubmit'));
2778 }
2779
2780 // init jQuery UI TabMenu
2781 self._containers[$containerID] = $tabMenu;
2782 $tabMenu.wcfTabs({
2783 active: false,
2784 activate: function(event, eventData) {
2785 var $panel = $(eventData.newPanel);
2786 var $container = $panel.closest('.tabMenuContainer');
2787
2788 // store currently selected item
2789 var $tabMenu = $container;
2790 while (true) {
2791 // do not trigger on init
2792 if ($tabMenu.data('isParent') === undefined) {
2793 break;
2794 }
2795
2796 if ($tabMenu.data('isParent')) {
2797 if ($tabMenu.data('store')) {
2798 $('#' + $tabMenu.data('store')).val($panel.attr('id'));
2799 }
2800
2801 break;
2802 }
2803 else {
2804 $tabMenu = $tabMenu.data('parent');
2805 }
2806 }
2807
2808 // set panel id as location hash
2809 if (WCF.TabMenu._didInit) {
2810 // do not update history if within an overlay
2811 if ($panel.data('inTabMenu') == undefined) {
2812 $panel.data('inTabMenu', ($panel.parents('.dialogContainer').length));
2813 }
2814
2815 if (!$panel.data('inTabMenu')) {
2816 if (window.history) {
2817 window.history.pushState(null, document.title, window.location.toString().replace(/#.+$/, '') + '#' + $panel.attr('id'));
2818 }
2819 else {
2820 location.hash = '#' + $panel.attr('id');
2821 }
2822 }
2823 }
2824 }
2825 });
2826
2827 $tabMenu.data('isParent', ($tabMenu.children('.tabMenuContainer, .tabMenuContent').length > 0)).data('parent', false);
2828 if (!$tabMenu.data('isParent')) {
2829 // check if we're a child element
2830 if ($tabMenu.parent().hasClass('tabMenuContainer')) {
2831 $tabMenu.data('parent', $tabMenu.parent());
2832 }
2833 }
2834 });
2835
2836 // try to resolve location hash
2837 if (!this._didInit) {
2838 this._selectActiveTab();
2839 $(window).bind('hashchange', $.proxy(this.selectTabs, this));
2840
2841 if (!this._selectErroneousTab()) {
2842 this.selectTabs();
2843 }
2844
2845 if ($.browser.mozilla && location.hash) {
2846 var $target = $(location.hash);
2847 if ($target.length && $target.hasClass('tabMenuContent')) {
2848 var $offset = $target.offset();
2849 window.scrollTo($offset.left, $offset.top);
2850 }
2851 }
2852 }
2853
2854 this._didInit = true;
2855 },
2856
2857 /**
2858 * Reloads the tab menus.
2859 */
2860 reload: function() {
2861 this._containers = { };
2862 this.init();
2863 },
2864
2865 /**
2866 * Force display of first erroneous tab and returns true if at least one
2867 * tab contains an error.
2868 *
2869 * @return boolean
2870 */
2871 _selectErroneousTab: function() {
2872 var $foundErrors = false;
2873 for (var $containerID in this._containers) {
2874 var $tabMenu = this._containers[$containerID];
2875
2876 if ($tabMenu.find('.formError').length) {
2877 $foundErrors = true;
2878
2879 if (!$tabMenu.data('isParent')) {
2880 while (true) {
2881 if ($tabMenu.data('parent') === false) {
2882 break;
2883 }
2884
2885 $tabMenu = $tabMenu.data('parent').wcfTabs('selectTab', $tabMenu.wcfIdentify());
2886 }
2887
2888 return true;
2889 }
2890 }
2891 }
2892
2893 // found an error in a non-nested tab menu
2894 if ($foundErrors) {
2895 for (var $containerID in this._containers) {
2896 var $tabMenu = this._containers[$containerID];
2897 var $formError = $tabMenu.find('.formError:eq(0)');
2898
2899 if ($formError.length) {
2900 // find the tab container
2901 $tabMenu.wcfTabs('selectTab', $formError.parents('.tabMenuContent').wcfIdentify());
2902
2903 while (true) {
2904 if ($tabMenu.data('parent') === false) {
2905 break;
2906 }
2907
2908 $tabMenu = $tabMenu.data('parent').wcfTabs('selectTab', $tabMenu.wcfIdentify());
2909 }
2910
2911 return true;
2912 }
2913 }
2914 }
2915
2916 return false;
2917 },
2918
2919 /**
2920 * Selects the active tab menu item.
2921 */
2922 _selectActiveTab: function() {
2923 for (var $containerID in this._containers) {
2924 var $tabMenu = this._containers[$containerID];
2925 if ($tabMenu.data('active')) {
2926 var $index = $tabMenu.data('active');
2927 var $subIndex = null;
2928 if (/-/.test($index)) {
2929 var $tmp = $index.split('-');
2930 $index = $tmp[0];
2931 $subIndex = $tmp[1];
2932 }
2933
2934 $tabMenu.find('.tabMenuContent').each(function(innerIndex, tabMenuItem) {
2935 var $tabMenuItem = $(tabMenuItem);
2936 if ($tabMenuItem.wcfIdentify() == $index) {
2937 $tabMenu.wcfTabs('select', innerIndex);
2938 if ($subIndex !== null) {
2939 if ($tabMenuItem.hasClass('tabMenuContainer')) {
2940 $tabMenuItem.wcfTabs('selectTab', $tabMenu.data('active'));
2941 }
2942 else {
2943 $tabMenu.wcfTabs('selectTab', $tabMenu.data('active'));
2944 }
2945 }
2946
2947 return false;
2948 }
2949 });
2950 }
2951 }
2952 },
2953
2954 /**
2955 * Resolves location hash to display tab menus.
2956 *
2957 * @return boolean
2958 */
2959 selectTabs: function() {
2960 if (location.hash) {
2961 var $hash = location.hash.substr(1);
2962
2963 // try to find matching tab menu container
2964 var $tabMenu = $('#' + $.wcfEscapeID($hash));
2965 if ($tabMenu.length === 1 && $tabMenu.hasClass('ui-tabs-panel')) {
2966 $tabMenu = $tabMenu.parent('.ui-tabs');
2967 if ($tabMenu.length) {
2968 $tabMenu.wcfTabs('selectTab', $hash);
2969
2970 // check if this is a nested tab menu
2971 if ($tabMenu.hasClass('ui-tabs-panel')) {
2972 $hash = $tabMenu.wcfIdentify();
2973 $tabMenu = $tabMenu.parent('.ui-tabs');
2974 if ($tabMenu.length) {
2975 $tabMenu.wcfTabs('selectTab', $hash);
2976 }
2977 }
2978
2979 return true;
2980 }
2981 }
2982 }
2983
2984 return false;
2985 }
2986 };
2987
2988 /**
2989 * Templates that may be fetched more than once with different variables.
2990 * Based upon ideas from Prototype's template.
2991 *
2992 * Usage:
2993 * var myTemplate = new WCF.Template('{$hello} World');
2994 * myTemplate.fetch({ hello: 'Hi' }); // Hi World
2995 * myTemplate.fetch({ hello: 'Hello' }); // Hello World
2996 *
2997 * my2ndTemplate = new WCF.Template('{@$html}{$html}');
2998 * my2ndTemplate.fetch({ html: '<b>Test</b>' }); // <b>Test</b>&lt;b&gt;Test&lt;/b&gt;
2999 *
3000 * var my3rdTemplate = new WCF.Template('You can use {literal}{$variable}{/literal}-Tags here');
3001 * my3rdTemplate.fetch({ variable: 'Not shown' }); // You can use {$variable}-Tags here
3002 *
3003 * @param template template-content
3004 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/template.js
3005 */
3006 WCF.Template = Class.extend({
3007 /**
3008 * Prepares template
3009 *
3010 * @param $template template-content
3011 */
3012 init: function(template) {
3013 var $literals = new WCF.Dictionary();
3014 var $tagID = 0;
3015
3016 // escape \ and ' and newlines
3017 template = template.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/(\r\n|\n|\r)/g, '\\n');
3018
3019 // save literal-tags
3020 template = template.replace(/\{literal\}(.*?)\{\/literal\}/g, $.proxy(function(match) {
3021 // hopefully no one uses this string in one of his templates
3022 var id = '@@@@@@@@@@@'+Math.random()+'@@@@@@@@@@@';
3023 $literals.add(id, match.replace(/\{\/?literal\}/g, ''));
3024
3025 return id;
3026 }, this));
3027
3028 // remove comments
3029 template = template.replace(/\{\*.*?\*\}/g, '');
3030
3031 var parseParameterList = function(parameterString) {
3032 var $chars = parameterString.split('');
3033 var $parameters = { };
3034 var $inName = true;
3035 var $name = '';
3036 var $value = '';
3037 var $doubleQuoted = false;
3038 var $singleQuoted = false;
3039 var $escaped = false;
3040
3041 for (var $i = 0, $max = $chars.length; $i < $max; $i++) {
3042 var $char = $chars[$i];
3043 if ($inName && $char != '=' && $char != ' ') $name += $char;
3044 else if ($inName && $char == '=') {
3045 $inName = false;
3046 $singleQuoted = false;
3047 $doubleQuoted = false;
3048 $escaped = false;
3049 }
3050 else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == ' ') {
3051 $inName = true;
3052 $parameters[$name] = $value;
3053 $value = $name = '';
3054 }
3055 else if (!$inName && $singleQuoted && !$escaped && $char == "'") {
3056 $singleQuoted = false;
3057 $value += $char;
3058 }
3059 else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == "'") {
3060 $singleQuoted = true;
3061 $value += $char;
3062 }
3063 else if (!$inName && $doubleQuoted && !$escaped && $char == '"') {
3064 $doubleQuoted = false;
3065 $value += $char;
3066 }
3067 else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == '"') {
3068 $doubleQuoted = true;
3069 $value += $char;
3070 }
3071 else if (!$inName && ($doubleQuoted || $singleQuoted) && !$escaped && $char == '\\') {
3072 $escaped = true;
3073 $value += $char;
3074 }
3075 else if (!$inName) {
3076 $escaped = false;
3077 $value += $char;
3078 }
3079 }
3080 $parameters[$name] = $value;
3081
3082 if ($doubleQuoted || $singleQuoted || $escaped) throw new Error('Syntax error in parameterList: "' + parameterString + '"');
3083
3084 return $parameters;
3085 };
3086
3087 var unescape = function(string) {
3088 return string.replace(/\\n/g, "\n").replace(/\\\\/g, '\\').replace(/\\'/g, "'");
3089 };
3090
3091 template = template.replace(/\{(\$[^\}]+?)\}/g, function(_, content) {
3092 content = unescape(content.replace(/\$([^.\[\(\)\]\s]+)/g, "(v['$1'])"));
3093
3094 return "' + WCF.String.escapeHTML(" + content + ") + '";
3095 })
3096 // Numeric Variable
3097 .replace(/\{#(\$[^\}]+?)\}/g, function(_, content) {
3098 content = unescape(content.replace(/\$([^.\[\(\)\]\s]+)/g, "(v['$1'])"));
3099
3100 return "' + WCF.String.formatNumeric(" + content + ") + '";
3101 })
3102 // Variable without escaping
3103 .replace(/\{@(\$[^\}]+?)\}/g, function(_, content) {
3104 content = unescape(content.replace(/\$([^.\[\(\)\]\s]+)/g, "(v['$1'])"));
3105
3106 return "' + " + content + " + '";
3107 })
3108 // {lang}foo{/lang}
3109 .replace(/\{lang\}(.+?)\{\/lang\}/g, function(_, content) {
3110 return "' + WCF.Language.get('" + content + "', v) + '";
3111 })
3112 // {include}
3113 .replace(/\{include (.+?)\}/g, function(_, content) {
3114 content = content.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
3115 var $parameters = parseParameterList(content);
3116
3117 if (typeof $parameters['file'] === 'undefined') throw new Error('Missing file attribute in include-tag');
3118
3119 $parameters['file'] = $parameters['file'].replace(/\$([^.\[\(\)\]\s]+)/g, "(v.$1)");
3120
3121 return "' + " + $parameters['file'] + ".fetch(v) + '";
3122 })
3123 // {if}
3124 .replace(/\{if (.+?)\}/g, function(_, content) {
3125 content = unescape(content.replace(/\$([^.\[\(\)\]\s]+)/g, "(v['$1'])"));
3126
3127 return "';\n" +
3128 "if (" + content + ") {\n" +
3129 " $output += '";
3130 })
3131 // {elseif}
3132 .replace(/\{else ?if (.+?)\}/g, function(_, content) {
3133 content = unescape(content.replace(/\$([^.\[\(\)\]\s]+)/g, "(v['$1'])"));
3134
3135 return "';\n" +
3136 "}\n" +
3137 "else if (" + content + ") {\n" +
3138 " $output += '";
3139 })
3140 // {implode}
3141 .replace(/\{implode (.+?)\}/g, function(_, content) {
3142 $tagID++;
3143
3144 content = content.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
3145 var $parameters = parseParameterList(content);
3146
3147 if (typeof $parameters['from'] === 'undefined') throw new Error('Missing from attribute in implode-tag');
3148 if (typeof $parameters['item'] === 'undefined') throw new Error('Missing item attribute in implode-tag');
3149 if (typeof $parameters['glue'] === 'undefined') $parameters['glue'] = "', '";
3150
3151 $parameters['from'] = $parameters['from'].replace(/\$([^.\[\(\)\]\s]+)/g, "(v.$1)");
3152
3153 return "';\n"+
3154 "var $implode_" + $tagID + " = false;\n" +
3155 "for ($implodeKey_" + $tagID + " in " + $parameters['from'] + ") {\n" +
3156 " v[" + $parameters['item'] + "] = " + $parameters['from'] + "[$implodeKey_" + $tagID + "];\n" +
3157 (typeof $parameters['key'] !== 'undefined' ? " v[" + $parameters['key'] + "] = $implodeKey_" + $tagID + ";\n" : "") +
3158 " if ($implode_" + $tagID + ") $output += " + $parameters['glue'] + ";\n" +
3159 " $implode_" + $tagID + " = true;\n" +
3160 " $output += '";
3161 })
3162 // {foreach}
3163 .replace(/\{foreach (.+?)\}/g, function(_, content) {
3164 $tagID++;
3165
3166 content = content.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
3167 var $parameters = parseParameterList(content);
3168
3169 if (typeof $parameters['from'] === 'undefined') throw new Error('Missing from attribute in foreach-tag');
3170 if (typeof $parameters['item'] === 'undefined') throw new Error('Missing item attribute in foreach-tag');
3171 $parameters['from'] = $parameters['from'].replace(/\$([^.\[\(\)\]\s]+)/g, "(v.$1)");
3172
3173 return "';\n" +
3174 "$foreach_"+$tagID+" = false;\n" +
3175 "for ($foreachKey_" + $tagID + " in " + $parameters['from'] + ") {\n" +
3176 " $foreach_"+$tagID+" = true;\n" +
3177 " break;\n" +
3178 "}\n" +
3179 "if ($foreach_"+$tagID+") {\n" +
3180 " for ($foreachKey_" + $tagID + " in " + $parameters['from'] + ") {\n" +
3181 " v[" + $parameters['item'] + "] = " + $parameters['from'] + "[$foreachKey_" + $tagID + "];\n" +
3182 (typeof $parameters['key'] !== 'undefined' ? " v[" + $parameters['key'] + "] = $foreachKey_" + $tagID + ";\n" : "") +
3183 " $output += '";
3184 })
3185 // {foreachelse}
3186 .replace(/\{foreachelse\}/g,
3187 "';\n" +
3188 " }\n" +
3189 "}\n" +
3190 "else {\n" +
3191 " {\n" +
3192 " $output += '"
3193 )
3194 // {/foreach}
3195 .replace(/\{\/foreach\}/g,
3196 "';\n" +
3197 " }\n" +
3198 "}\n" +
3199 "$output += '"
3200 )
3201 // {else}
3202 .replace(/\{else\}/g,
3203 "';\n" +
3204 "}\n" +
3205 "else {\n" +
3206 " $output += '"
3207 )
3208 // {/if} and {/implode}
3209 .replace(/\{\/(if|implode)\}/g,
3210 "';\n" +
3211 "}\n" +
3212 "$output += '"
3213 );
3214
3215 // call callback
3216 for (var key in WCF.Template.callbacks) {
3217 template = WCF.Template.callbacks[key](template);
3218 }
3219
3220 // insert delimiter tags
3221 template = template.replace('{ldelim}', '{').replace('{rdelim}', '}');
3222
3223 $literals.each(function(pair) {
3224 template = template.replace(pair.key, pair.value);
3225 });
3226
3227 template = "$output += '" + template + "';";
3228
3229 try {
3230 this.fetch = new Function("v", "v = window.$.extend({}, v, { __wcf: window.WCF, __window: window }); var $output = ''; " + template + ' return $output;');
3231 }
3232 catch (e) {
3233 console.debug("var $output = ''; " + template + ' return $output;');
3234 throw e;
3235 }
3236 },
3237
3238 /**
3239 * Fetches the template with the given variables.
3240 *
3241 * @param v variables to insert
3242 * @return parsed template
3243 */
3244 fetch: function(v) {
3245 // this will be replaced in the init function
3246 }
3247 });
3248
3249 /**
3250 * Array of callbacks that will be called after parsing the included tags. Only applies to Templates compiled after the callback was added.
3251 *
3252 * @var array<Function>
3253 */
3254 WCF.Template.callbacks = [ ];
3255
3256 /**
3257 * Toggles options.
3258 *
3259 * @param string element
3260 * @param array showItems
3261 * @param array hideItems
3262 * @param function callback
3263 */
3264 WCF.ToggleOptions = Class.extend({
3265 /**
3266 * target item
3267 *
3268 * @var jQuery
3269 */
3270 _element: null,
3271
3272 /**
3273 * list of items to be shown
3274 *
3275 * @var array
3276 */
3277 _showItems: [],
3278
3279 /**
3280 * list of items to be hidden
3281 *
3282 * @var array
3283 */
3284 _hideItems: [],
3285
3286 /**
3287 * callback after options were toggled
3288 *
3289 * @var function
3290 */
3291 _callback: null,
3292
3293 /**
3294 * Initializes option toggle.
3295 *
3296 * @param string element
3297 * @param array showItems
3298 * @param array hideItems
3299 * @param function callback
3300 */
3301 init: function(element, showItems, hideItems, callback) {
3302 this._element = $('#' + element);
3303 this._showItems = showItems;
3304 this._hideItems = hideItems;
3305 if (callback !== undefined) {
3306 this._callback = callback;
3307 }
3308
3309 // bind event
3310 this._element.click($.proxy(this._toggle, this));
3311
3312 // execute toggle on init
3313 this._toggle();
3314 },
3315
3316 /**
3317 * Toggles items.
3318 */
3319 _toggle: function() {
3320 if (!this._element.prop('checked')) return;
3321
3322 for (var $i = 0, $length = this._showItems.length; $i < $length; $i++) {
3323 var $item = this._showItems[$i];
3324
3325 $('#' + $item).show();
3326 }
3327
3328 for (var $i = 0, $length = this._hideItems.length; $i < $length; $i++) {
3329 var $item = this._hideItems[$i];
3330
3331 $('#' + $item).hide();
3332 }
3333
3334 if (this._callback !== null) {
3335 this._callback();
3336 }
3337 }
3338 });
3339
3340 /**
3341 * Namespace for all kind of collapsible containers.
3342 */
3343 WCF.Collapsible = {};
3344
3345 /**
3346 * Simple implementation for collapsible content, neither does it
3347 * store its state nor does it allow AJAX callbacks to fetch content.
3348 */
3349 WCF.Collapsible.Simple = {
3350 /**
3351 * Initializes collapsibles.
3352 */
3353 init: function() {
3354 $('.jsCollapsible').each($.proxy(function(index, button) {
3355 this._initButton(button);
3356 }, this));
3357 },
3358
3359 /**
3360 * Binds an event listener on all buttons triggering the collapsible.
3361 *
3362 * @param object button
3363 */
3364 _initButton: function(button) {
3365 var $button = $(button);
3366 var $isOpen = $button.data('isOpen');
3367
3368 if (!$isOpen) {
3369 // hide container on init
3370 $('#' + $button.data('collapsibleContainer')).hide();
3371 }
3372
3373 $button.click($.proxy(this._toggle, this));
3374 },
3375
3376 /**
3377 * Toggles collapsible containers on click.
3378 *
3379 * @param object event
3380 */
3381 _toggle: function(event) {
3382 var $button = $(event.currentTarget);
3383 var $isOpen = $button.data('isOpen');
3384 var $target = $('#' + $.wcfEscapeID($button.data('collapsibleContainer')));
3385
3386 if ($isOpen) {
3387 $target.stop().wcfBlindOut('vertical', $.proxy(function() {
3388 this._toggleImage($button);
3389 }, this));
3390 $isOpen = false;
3391 }
3392 else {
3393 $target.stop().wcfBlindIn('vertical', $.proxy(function() {
3394 this._toggleImage($button);
3395 }, this));
3396 $isOpen = true;
3397 }
3398
3399 $button.data('isOpen', $isOpen);
3400
3401 // suppress event
3402 event.stopPropagation();
3403 return false;
3404 },
3405
3406 /**
3407 * Toggles image of target button.
3408 *
3409 * @param jQuery button
3410 */
3411 _toggleImage: function(button) {
3412 var $icon = button.find('span.icon');
3413 if (button.data('isOpen')) {
3414 $icon.removeClass('fa-chevron-right').addClass('fa-chevron-down');
3415 }
3416 else {
3417 $icon.removeClass('fa-chevron-down').addClass('fa-chevron-right');
3418 }
3419 }
3420 };
3421
3422 /**
3423 * Basic implementation for collapsible containers with AJAX support. Results for open
3424 * and closed state will be cached.
3425 *
3426 * @param string className
3427 */
3428 WCF.Collapsible.Remote = Class.extend({
3429 /**
3430 * class name
3431 * @var string
3432 */
3433 _className: '',
3434
3435 /**
3436 * list of active containers
3437 * @var object
3438 */
3439 _containers: {},
3440
3441 /**
3442 * container meta data
3443 * @var object
3444 */
3445 _containerData: {},
3446
3447 /**
3448 * action proxy
3449 * @var WCF.Action.Proxy
3450 */
3451 _proxy: null,
3452
3453 /**
3454 * Initializes the controller for collapsible containers with AJAX support.
3455 *
3456 * @param string className
3457 */
3458 init: function(className) {
3459 this._className = className;
3460 this._proxy = new WCF.Action.Proxy({
3461 success: $.proxy(this._success, this)
3462 });
3463
3464 // initialize each container
3465 this._init();
3466
3467 WCF.DOMNodeInsertedHandler.addCallback('WCF.Collapsible.Remote', $.proxy(this._init, this));
3468 },
3469
3470 /**
3471 * Initializes a collapsible container.
3472 *
3473 * @param string containerID
3474 */
3475 _init: function(containerID) {
3476 this._getContainers().each($.proxy(function(index, container) {
3477 var $container = $(container);
3478 var $containerID = $container.wcfIdentify();
3479
3480 if (this._containers[$containerID] === undefined) {
3481 this._containers[$containerID] = $container;
3482
3483 this._initContainer($containerID);
3484 }
3485 }, this));
3486 },
3487
3488 /**
3489 * Initializes a collapsible container.
3490 *
3491 * @param string containerID
3492 */
3493 _initContainer: function(containerID) {
3494 var $target = this._getTarget(containerID);
3495 var $buttonContainer = this._getButtonContainer(containerID);
3496 var $button = this._createButton(containerID, $buttonContainer);
3497
3498 // store container meta data
3499 this._containerData[containerID] = {
3500 button: $button,
3501 buttonContainer: $buttonContainer,
3502 isOpen: this._containers[containerID].data('isOpen'),
3503 target: $target
3504 };
3505
3506 // add 'jsCollapsed' CSS class
3507 if (!this._containers[containerID].data('isOpen')) {
3508 $('#' + containerID).addClass('jsCollapsed');
3509 }
3510 },
3511
3512 /**
3513 * Returns a collection of collapsible containers.
3514 *
3515 * @return jQuery
3516 */
3517 _getContainers: function() { },
3518
3519 /**
3520 * Returns the target element for current collapsible container.
3521 *
3522 * @param integer containerID
3523 * @return jQuery
3524 */
3525 _getTarget: function(containerID) { },
3526
3527 /**
3528 * Returns the button container for current collapsible container.
3529 *
3530 * @param integer containerID
3531 * @return jQuery
3532 */
3533 _getButtonContainer: function(containerID) { },
3534
3535 /**
3536 * Creates the toggle button.
3537 *
3538 * @param integer containerID
3539 * @param jQuery buttonContainer
3540 */
3541 _createButton: function(containerID, buttonContainer) {
3542 var $isOpen = this._containers[containerID].data('isOpen');
3543 var $button = $('<span class="collapsibleButton jsTooltip pointer icon icon16 fa-chevron-down" title="'+WCF.Language.get('wcf.global.button.collapsible')+'">').prependTo(buttonContainer);
3544 $button.data('containerID', containerID).click($.proxy(this._toggleContainer, this));
3545
3546 return $button;
3547 },
3548
3549 /**
3550 * Toggles a container.
3551 *
3552 * @param object event
3553 */
3554 _toggleContainer: function(event) {
3555 var $button = $(event.currentTarget);
3556 var $containerID = $button.data('containerID');
3557 var $isOpen = this._containerData[$containerID].isOpen;
3558 var $state = ($isOpen) ? 'open' : 'close';
3559 var $newState = ($isOpen) ? 'close' : 'open';
3560
3561 // fetch content state via AJAX
3562 this._proxy.setOption('data', {
3563 actionName: 'loadContainer',
3564 className: this._className,
3565 interfaceName: 'wcf\\data\\ILoadableContainerAction',
3566 objectIDs: [ this._getObjectID($containerID) ],
3567 parameters: $.extend(true, {
3568 containerID: $containerID,
3569 currentState: $state,
3570 newState: $newState
3571 }, this._getAdditionalParameters($containerID))
3572 });
3573 this._proxy.sendRequest();
3574
3575 // toogle 'jsCollapsed' CSS class
3576 $('#' + $containerID).toggleClass('jsCollapsed');
3577
3578 // set spinner for current button
3579 // this._exchangeIcon($button);
3580 },
3581
3582 /**
3583 * Exchanges button icon.
3584 *
3585 * @param jQuery button
3586 * @param string newIcon
3587 */
3588 _exchangeIcon: function(button, newIcon) {
3589 newIcon = newIcon || 'spinner';
3590 button.removeClass('fa-chevron-down fa-chevron-right fa-spinner').addClass('fa-' + newIcon);
3591 },
3592
3593 /**
3594 * Returns the object id for current container.
3595 *
3596 * @param integer containerID
3597 * @return integer
3598 */
3599 _getObjectID: function(containerID) {
3600 return $('#' + containerID).data('objectID');
3601 },
3602
3603 /**
3604 * Returns additional parameters.
3605 *
3606 * @param integer containerID
3607 * @return object
3608 */
3609 _getAdditionalParameters: function(containerID) {
3610 return {};
3611 },
3612
3613 /**
3614 * Updates container content.
3615 *
3616 * @param integer containerID
3617 * @param string newContent
3618 * @param string newState
3619 */
3620 _updateContent: function(containerID, newContent, newState) {
3621 this._containerData[containerID].target.html(newContent);
3622 },
3623
3624 /**
3625 * Sets content upon successfull AJAX request.
3626 *
3627 * @param object data
3628 * @param string textStatus
3629 * @param jQuery jqXHR
3630 */
3631 _success: function(data, textStatus, jqXHR) {
3632 // validate container id
3633 if (!data.returnValues.containerID) return;
3634 var $containerID = data.returnValues.containerID;
3635
3636 // check if container id is known
3637 if (!this._containers[$containerID]) return;
3638
3639 // update content storage
3640 this._containerData[$containerID].isOpen = (data.returnValues.isOpen) ? true : false;
3641 var $newState = (data.returnValues.isOpen) ? 'open' : 'close';
3642
3643 // update container content
3644 this._updateContent($containerID, $.trim(data.returnValues.content), $newState);
3645
3646 // update icon
3647 this._exchangeIcon(this._containerData[$containerID].button, (data.returnValues.isOpen ? 'chevron-down' : 'chevron-right'));
3648 }
3649 });
3650
3651 /**
3652 * Basic implementation for collapsible containers with AJAX support. Requires collapsible
3653 * content to be available in DOM already, if you want to load content on the fly use
3654 * WCF.Collapsible.Remote instead.
3655 */
3656 WCF.Collapsible.SimpleRemote = WCF.Collapsible.Remote.extend({
3657 /**
3658 * Initializes an AJAX-based collapsible handler.
3659 *
3660 * @param string className
3661 */
3662 init: function(className) {
3663 this._super(className);
3664
3665 // override settings for action proxy
3666 this._proxy = new WCF.Action.Proxy({
3667 showLoadingOverlay: false
3668 });
3669 },
3670
3671 /**
3672 * @see WCF.Collapsible.Remote._initContainer()
3673 */
3674 _initContainer: function(containerID) {
3675 this._super(containerID);
3676
3677 // hide container on init if applicable
3678 if (!this._containerData[containerID].isOpen) {
3679 this._containerData[containerID].target.hide();
3680 this._exchangeIcon(this._containerData[containerID].button, 'chevron-right');
3681 }
3682 },
3683
3684 /**
3685 * Toggles container visibility.
3686 *
3687 * @param object event
3688 */
3689 _toggleContainer: function(event) {
3690 var $button = $(event.currentTarget);
3691 var $containerID = $button.data('containerID');
3692 var $isOpen = this._containerData[$containerID].isOpen;
3693 var $currentState = ($isOpen) ? 'open' : 'close';
3694 var $newState = ($isOpen) ? 'close' : 'open';
3695
3696 this._proxy.setOption('data', {
3697 actionName: 'toggleContainer',
3698 className: this._className,
3699 interfaceName: 'wcf\\data\\IToggleContainerAction',
3700 objectIDs: [ this._getObjectID($containerID) ],
3701 parameters: $.extend(true, {
3702 containerID: $containerID,
3703 currentState: $currentState,
3704 newState: $newState
3705 }, this._getAdditionalParameters($containerID))
3706 });
3707 this._proxy.sendRequest();
3708
3709 // exchange icon
3710 this._exchangeIcon(this._containerData[$containerID].button, ($newState === 'open' ? 'chevron-down' : 'chevron-right'));
3711
3712 // toggle container
3713 if ($newState === 'open') {
3714 this._containerData[$containerID].target.show();
3715 }
3716 else {
3717 this._containerData[$containerID].target.hide();
3718 }
3719
3720 // toogle 'jsCollapsed' CSS class
3721 $('#' + $containerID).toggleClass('jsCollapsed');
3722
3723 // update container data
3724 this._containerData[$containerID].isOpen = ($newState === 'open' ? true : false);
3725 }
3726 });
3727
3728 /**
3729 * Holds userdata of the current user
3730 */
3731 WCF.User = {
3732 /**
3733 * id of the active user
3734 * @var integer
3735 */
3736 userID: 0,
3737
3738 /**
3739 * name of the active user
3740 * @var string
3741 */
3742 username: '',
3743
3744 /**
3745 * Initializes userdata
3746 *
3747 * @param integer userID
3748 * @param string username
3749 */
3750 init: function(userID, username) {
3751 this.userID = userID;
3752 this.username = username;
3753 }
3754 };
3755
3756 /**
3757 * Namespace for effect-related functions.
3758 */
3759 WCF.Effect = {};
3760
3761 /**
3762 * Scrolls to a specific element offset, optionally handling menu height.
3763 */
3764 WCF.Effect.Scroll = Class.extend({
3765 /**
3766 * Scrolls to a specific element offset.
3767 *
3768 * @param jQuery element
3769 * @param boolean excludeMenuHeight
3770 * @param boolean disableAnimation
3771 * @return boolean
3772 */
3773 scrollTo: function(element, excludeMenuHeight, disableAnimation) {
3774 if (!element.length) {
3775 return true;
3776 }
3777
3778 var $elementOffset = element.getOffsets('offset').top;
3779 var $documentHeight = $(document).height();
3780 var $windowHeight = $(window).height();
3781
3782 // handles menu height
3783 /*if (excludeMenuHeight) {
3784 $elementOffset = Math.max($elementOffset - $('#topMenu').outerHeight(), 0);
3785 }*/
3786
3787 if ($elementOffset > $documentHeight - $windowHeight) {
3788 $elementOffset = $documentHeight - $windowHeight;
3789 if ($elementOffset < 0) {
3790 $elementOffset = 0;
3791 }
3792 }
3793
3794 if (disableAnimation === true) {
3795 $('html,body').scrollTop($elementOffset);
3796 }
3797 else {
3798 $('html,body').animate({ scrollTop: $elementOffset }, 400, function (x, t, b, c, d) {
3799 return -c * ( ( t = t / d - 1 ) * t * t * t - 1) + b;
3800 });
3801 }
3802
3803 return false;
3804 }
3805 });
3806
3807 /**
3808 * Creates a smooth scroll effect.
3809 */
3810 WCF.Effect.SmoothScroll = WCF.Effect.Scroll.extend({
3811 /**
3812 * Initializes effect.
3813 */
3814 init: function() {
3815 var self = this;
3816 $(document).on('click', 'a[href$=#top],a[href$=#bottom]', function() {
3817 var $target = $(this.hash);
3818 self.scrollTo($target, true);
3819
3820 return false;
3821 });
3822 }
3823 });
3824
3825 /**
3826 * Handles clicks outside an overlay, hitting body-tag through bubbling.
3827 *
3828 * You should always remove callbacks before disposing the attached element,
3829 * preventing errors from blocking the iteration. Furthermore you should
3830 * always handle clicks on your overlay's container and return 'false' to
3831 * prevent bubbling.
3832 *
3833 * @deprecated 2.2 - please use `Ui/CloseOverlay` instead
3834 */
3835 WCF.CloseOverlayHandler = {
3836 /**
3837 * Adds a new callback.
3838 *
3839 * @param string identifier
3840 * @param object callback
3841 */
3842 addCallback: function(identifier, callback) {
3843 require(['Ui/CloseOverlay'], function(UiCloseOverlay) {
3844 UiCloseOverlay.add(identifier, callback);
3845 });
3846 },
3847
3848 /**
3849 * Removes a callback from list.
3850 *
3851 * @param string identifier
3852 */
3853 removeCallback: function(identifier) {
3854 require(['Ui/CloseOverlay'], function(UiCloseOverlay) {
3855 UiCloseOverlay.remove(identifier);
3856 });
3857 },
3858
3859 /**
3860 * Triggers the callbacks programmatically.
3861 */
3862 forceExecution: function() {
3863 require(['Ui/CloseOverlay'], function(UiCloseOverlay) {
3864 UiCloseOverlay.execute();
3865 });
3866 }
3867 };
3868
3869 /**
3870 * @deprecated Use WoltLab/WCF/Dom/Change/Listener
3871 */
3872 WCF.DOMNodeInsertedHandler = {
3873 addCallback: function(identifier, callback) {
3874 require(['WoltLab/WCF/Dom/Change/Listener'], function (ChangeListener) {
3875 ChangeListener.add('__legacy__', callback);
3876 });
3877 },
3878 _executeCallbacks: function() {
3879 require(['WoltLab/WCF/Dom/Change/Listener'], function (ChangeListener) {
3880 ChangeListener.trigger();
3881 });
3882 },
3883 execute: function() {
3884 this._executeCallbacks();
3885 }
3886 };
3887
3888 /**
3889 * Notifies objects once a DOM node was removed.
3890 */
3891 WCF.DOMNodeRemovedHandler = {
3892 /**
3893 * list of callbacks
3894 * @var WCF.Dictionary
3895 */
3896 _callbacks: new WCF.Dictionary(),
3897
3898 /**
3899 * prevent infinite loop if a callback manipulates DOM
3900 * @var boolean
3901 */
3902 _isExecuting: false,
3903
3904 /**
3905 * indicates that overlay handler is listening to DOMNodeRemoved events on body-tag
3906 * @var boolean
3907 */
3908 _isListening: false,
3909
3910 /**
3911 * Adds a new callback.
3912 *
3913 * @param string identifier
3914 * @param object callback
3915 */
3916 addCallback: function(identifier, callback) {
3917 this._bindListener();
3918
3919 if (this._callbacks.isset(identifier)) {
3920 console.debug("[WCF.DOMNodeRemovedHandler] identifier '" + identifier + "' is already bound to a callback");
3921 return false;
3922 }
3923
3924 this._callbacks.add(identifier, callback);
3925 },
3926
3927 /**
3928 * Removes a callback from list.
3929 *
3930 * @param string identifier
3931 */
3932 removeCallback: function(identifier) {
3933 if (this._callbacks.isset(identifier)) {
3934 this._callbacks.remove(identifier);
3935 }
3936 },
3937
3938 /**
3939 * Binds click event handler.
3940 */
3941 _bindListener: function() {
3942 if (this._isListening) return;
3943
3944 if (window.MutationObserver) {
3945 var $mutationObserver = new MutationObserver((function(mutations) {
3946 var $triggerEvent = false;
3947
3948 mutations.forEach((function(mutation) {
3949 if (mutation.removedNodes.length) {
3950 $triggerEvent = true;
3951 }
3952 }).bind(this));
3953
3954 if ($triggerEvent) {
3955 this._executeCallbacks({ });
3956 }
3957 }).bind(this));
3958
3959 $mutationObserver.observe(document.body, {
3960 childList: true,
3961 subtree: true
3962 });
3963 }
3964 else {
3965 $(document).bind('DOMNodeRemoved', $.proxy(this._executeCallbacks, this));
3966 }
3967
3968 this._isListening = true;
3969 },
3970
3971 /**
3972 * Executes callbacks if a DOM node is removed.
3973 */
3974 _executeCallbacks: function(event) {
3975 if (this._isExecuting) return;
3976
3977 // do not track events while executing callbacks
3978 this._isExecuting = true;
3979
3980 this._callbacks.each(function(pair) {
3981 // execute callback
3982 pair.value(event);
3983 });
3984
3985 // enable listener again
3986 this._isExecuting = false;
3987 }
3988 };
3989
3990 /**
3991 * Namespace for option handlers.
3992 */
3993 WCF.Option = { };
3994
3995 /**
3996 * Handles option selection.
3997 */
3998 WCF.Option.Handler = Class.extend({
3999 /**
4000 * Initializes the WCF.Option.Handler class.
4001 */
4002 init: function() {
4003 this._initOptions();
4004
4005 WCF.DOMNodeInsertedHandler.addCallback('WCF.Option.Handler', $.proxy(this._initOptions, this));
4006 },
4007
4008 /**
4009 * Initializes all options.
4010 */
4011 _initOptions: function() {
4012 $('.jsEnablesOptions').each($.proxy(this._initOption, this));
4013 },
4014
4015 /**
4016 * Initializes an option.
4017 *
4018 * @param integer index
4019 * @param object option
4020 */
4021 _initOption: function(index, option) {
4022 // execute action on init
4023 this._change(option);
4024
4025 // bind event listener
4026 $(option).change($.proxy(this._handleChange, this));
4027 },
4028
4029 /**
4030 * Applies whenever an option is changed.
4031 *
4032 * @param object event
4033 */
4034 _handleChange: function(event) {
4035 this._change($(event.target));
4036 },
4037
4038 /**
4039 * Enables or disables options on option value change.
4040 *
4041 * @param object option
4042 */
4043 _change: function(option) {
4044 option = $(option);
4045
4046 var $disableOptions = eval(option.data('disableOptions'));
4047 var $enableOptions = eval(option.data('enableOptions'));
4048
4049 // determine action by type
4050 switch(option.getTagName()) {
4051 case 'input':
4052 switch(option.attr('type')) {
4053 case 'checkbox':
4054 this._execute(option.prop('checked'), $disableOptions, $enableOptions);
4055 break;
4056
4057 case 'radio':
4058 if (option.prop('checked')) {
4059 var isActive = true;
4060 if (option.data('isBoolean') && option.val() != 1) {
4061 isActive = false;
4062 }
4063
4064 this._execute(isActive, $disableOptions, $enableOptions);
4065 }
4066 break;
4067 }
4068 break;
4069
4070 case 'select':
4071 var $value = option.val();
4072 var $disableOptions = $enableOptions = [];
4073
4074 if (option.data('disableOptions').length > 0) {
4075 for (var $index in option.data('disableOptions')) {
4076 var $item = option.data('disableOptions')[$index];
4077
4078 if ($item.value == $value) {
4079 $disableOptions.push($item.option);
4080 }
4081 }
4082 }
4083
4084 if (option.data('enableOptions').length > 0) {
4085 for (var $index in option.data('enableOptions')) {
4086 var $item = option.data('enableOptions')[$index];
4087
4088 if ($item.value == $value) {
4089 $enableOptions.push($item.option);
4090 }
4091 }
4092 }
4093
4094 this._execute(true, $disableOptions, $enableOptions);
4095 break;
4096 }
4097 },
4098
4099 /**
4100 * Enables or disables options.
4101 *
4102 * @param boolean isActive
4103 * @param array disableOptions
4104 * @param array enableOptions
4105 */
4106 _execute: function(isActive, disableOptions, enableOptions) {
4107 if (disableOptions.length > 0) {
4108 for (var $i = 0, $size = disableOptions.length; $i < $size; $i++) {
4109 var $target = disableOptions[$i];
4110 if ($.wcfIsset($target)) {
4111 this._enableOption($target, !isActive);
4112 }
4113 else {
4114 var $dl = $('.' + $target + 'Input');
4115 if ($dl.length) {
4116 this._enableOptions($dl.children('dd').find('input, select, textarea'), !isActive);
4117 }
4118 }
4119 }
4120 }
4121
4122 if (enableOptions.length > 0) {
4123 for (var $i = 0, $size = enableOptions.length; $i < $size; $i++) {
4124 var $target = enableOptions[$i];
4125 if ($.wcfIsset($target)) {
4126 this._enableOption($target, isActive);
4127 }
4128 else {
4129 var $dl = $('.' + $target + 'Input');
4130 if ($dl.length) {
4131 this._enableOptions($dl.children('dd').find('input, select, textarea'), isActive);
4132 }
4133 }
4134 }
4135 }
4136 },
4137
4138 /**
4139 * Enables/Disables an option.
4140 *
4141 * @param string target
4142 * @param boolean enable
4143 */
4144 _enableOption: function(target, enable) {
4145 this._enableOptionElement($('#' + $.wcfEscapeID(target)), enable);
4146 },
4147
4148 /**
4149 * Enables/Disables an option element.
4150 *
4151 * @param string target
4152 * @param boolean enable
4153 */
4154 _enableOptionElement: function(element, enable) {
4155 element = $(element);
4156 var $tagName = element.getTagName();
4157
4158 if ($tagName == 'select' || ($tagName == 'input' && (element.attr('type') == 'checkbox' || element.attr('type') == 'file' || element.attr('type') == 'radio'))) {
4159 if (enable) element.enable();
4160 else element.disable();
4161 }
4162 else {
4163 if (enable) element.removeAttr('readonly');
4164 else element.attr('readonly', true);
4165 }
4166
4167 if (enable) {
4168 element.closest('dl').removeClass('disabled');
4169 }
4170 else {
4171 element.closest('dl').addClass('disabled');
4172 }
4173 },
4174
4175 /**
4176 * Enables/Disables an option consisting of multiple form elements.
4177 *
4178 * @param string target
4179 * @param boolean enable
4180 */
4181 _enableOptions: function(targets, enable) {
4182 for (var $i = 0, $length = targets.length; $i < $length; $i++) {
4183 this._enableOptionElement(targets[$i], enable);
4184 }
4185 }
4186 });
4187
4188 WCF.PageVisibilityHandler = {
4189 /**
4190 * list of callbacks
4191 * @var WCF.Dictionary
4192 */
4193 _callbacks: new WCF.Dictionary(),
4194
4195 /**
4196 * indicates that event listeners are bound
4197 * @var boolean
4198 */
4199 _isListening: false,
4200
4201 /**
4202 * name of window's hidden property
4203 * @var string
4204 */
4205 _hiddenFieldName: '',
4206
4207 /**
4208 * Adds a new callback.
4209 *
4210 * @param string identifier
4211 * @param object callback
4212 */
4213 addCallback: function(identifier, callback) {
4214 this._bindListener();
4215
4216 if (this._callbacks.isset(identifier)) {
4217 console.debug("[WCF.PageVisibilityHandler] identifier '" + identifier + "' is already bound to a callback");
4218 return false;
4219 }
4220
4221 this._callbacks.add(identifier, callback);
4222 },
4223
4224 /**
4225 * Removes a callback from list.
4226 *
4227 * @param string identifier
4228 */
4229 removeCallback: function(identifier) {
4230 if (this._callbacks.isset(identifier)) {
4231 this._callbacks.remove(identifier);
4232 }
4233 },
4234
4235 /**
4236 * Binds click event handler.
4237 */
4238 _bindListener: function() {
4239 if (this._isListening) return;
4240
4241 var $eventName = null;
4242 if (typeof document.hidden !== "undefined") {
4243 this._hiddenFieldName = "hidden";
4244 $eventName = "visibilitychange";
4245 }
4246 else if (typeof document.mozHidden !== "undefined") {
4247 this._hiddenFieldName = "mozHidden";
4248 $eventName = "mozvisibilitychange";
4249 }
4250 else if (typeof document.msHidden !== "undefined") {
4251 this._hiddenFieldName = "msHidden";
4252 $eventName = "msvisibilitychange";
4253 }
4254 else if (typeof document.webkitHidden !== "undefined") {
4255 this._hiddenFieldName = "webkitHidden";
4256 $eventName = "webkitvisibilitychange";
4257 }
4258
4259 if ($eventName === null) {
4260 console.debug("[WCF.PageVisibilityHandler] This browser does not support the page visibility API.");
4261 }
4262 else {
4263 $(document).on($eventName, $.proxy(this._executeCallbacks, this));
4264 }
4265
4266 this._isListening = true;
4267 },
4268
4269 /**
4270 * Executes callbacks if page is hidden/visible again.
4271 */
4272 _executeCallbacks: function(event) {
4273 if (this._isExecuting) return;
4274
4275 // do not track events while executing callbacks
4276 this._isExecuting = true;
4277
4278 var $state = document[this._hiddenFieldName];
4279 this._callbacks.each(function(pair) {
4280 // execute callback
4281 pair.value($state);
4282 });
4283
4284 // enable listener again
4285 this._isExecuting = false;
4286 }
4287 };
4288
4289 /**
4290 * Namespace for table related classes.
4291 */
4292 WCF.Table = { };
4293
4294 /**
4295 * Handles empty tables which can be used in combination with WCF.Action.Proxy.
4296 */
4297 WCF.Table.EmptyTableHandler = Class.extend({
4298 /**
4299 * handler options
4300 * @var object
4301 */
4302 _options: {},
4303
4304 /**
4305 * class name of the relevant rows
4306 * @var string
4307 */
4308 _rowClassName: '',
4309
4310 /**
4311 * Initalizes a new WCF.Table.EmptyTableHandler object.
4312 *
4313 * @param jQuery tableContainer
4314 * @param string rowClassName
4315 * @param object options
4316 */
4317 init: function(tableContainer, rowClassName, options) {
4318 this._rowClassName = rowClassName;
4319 this._tableContainer = tableContainer;
4320
4321 this._options = $.extend(true, {
4322 emptyMessage: null,
4323 messageType: 'info',
4324 refreshPage: false,
4325 updatePageNumber: false
4326 }, options || { });
4327
4328 WCF.DOMNodeRemovedHandler.addCallback('WCF.Table.EmptyTableHandler.' + rowClassName, $.proxy(this._remove, this));
4329 },
4330
4331 /**
4332 * Returns the current number of table rows.
4333 *
4334 * @return integer
4335 */
4336 _getRowCount: function() {
4337 return this._tableContainer.find('table tr.' + this._rowClassName).length;
4338 },
4339
4340 /**
4341 * Handles an empty table.
4342 */
4343 _handleEmptyTable: function() {
4344 if (this._options.emptyMessage) {
4345 // insert message
4346 this._tableContainer.replaceWith($('<p />').addClass(this._options.messageType).text(this._options.emptyMessage));
4347 }
4348 else if (this._options.refreshPage) {
4349 // refresh page
4350 if (this._options.updatePageNumber) {
4351 // calculate the new page number
4352 var pageNumberURLComponents = window.location.href.match(/(\?|&)pageNo=(\d+)/g);
4353 if (pageNumberURLComponents) {
4354 var currentPageNumber = pageNumberURLComponents[pageNumberURLComponents.length - 1].match(/\d+/g);
4355 if (this._options.updatePageNumber > 0) {
4356 currentPageNumber++;
4357 }
4358 else {
4359 currentPageNumber--;
4360 }
4361
4362 window.location = window.location.href.replace(pageNumberURLComponents[pageNumberURLComponents.length - 1], pageNumberURLComponents[pageNumberURLComponents.length - 1][0] + 'pageNo=' + currentPageNumber);
4363 }
4364 }
4365 else {
4366 window.location.reload();
4367 }
4368 }
4369 else {
4370 // simply remove the table container
4371 this._tableContainer.remove();
4372 }
4373 },
4374
4375 /**
4376 * Handles the removal of a DOM node.
4377 */
4378 _remove: function(event) {
4379 if ($.getLength(event)) {
4380 var element = $(event.target);
4381
4382 // check if DOM element is relevant
4383 if (element.hasClass(this._rowClassName)) {
4384 var tbody = element.parents('tbody:eq(0)');
4385
4386 // check if table will be empty if DOM node is removed
4387 if (tbody.children('tr').length == 1) {
4388 this._handleEmptyTable();
4389 }
4390 }
4391 }
4392 else if (!this._getRowCount()) {
4393 this._handleEmptyTable();
4394 }
4395 }
4396 });
4397
4398 /**
4399 * Namespace for search related classes.
4400 */
4401 WCF.Search = {};
4402
4403 /**
4404 * Performs a quick search.
4405 *
4406 * @deprecated 2.2 - please use `WoltLab/WCF/Ui/Search/Input` instead
4407 */
4408 WCF.Search.Base = Class.extend({
4409 /**
4410 * notification callback
4411 * @var object
4412 */
4413 _callback: null,
4414
4415 /**
4416 * represents index of search string (relative to original caret position)
4417 * @var integer
4418 */
4419 _caretAt: -1,
4420
4421 /**
4422 * class name
4423 * @var string
4424 */
4425 _className: '',
4426
4427 /**
4428 * comma seperated list
4429 * @var boolean
4430 */
4431 _commaSeperated: false,
4432
4433 /**
4434 * delay in miliseconds before a request is send to the server
4435 * @var integer
4436 */
4437 _delay: 0,
4438
4439 /**
4440 * list with values that are excluded from seaching
4441 * @var array
4442 */
4443 _excludedSearchValues: [],
4444
4445 /**
4446 * count of available results
4447 * @var integer
4448 */
4449 _itemCount: 0,
4450
4451 /**
4452 * item index, -1 if none is selected
4453 * @var integer
4454 */
4455 _itemIndex: -1,
4456
4457 /**
4458 * result list
4459 * @var jQuery
4460 */
4461 _list: null,
4462
4463 /**
4464 * old search string, used for comparison
4465 * @var array<string>
4466 */
4467 _oldSearchString: [ ],
4468
4469 /**
4470 * action proxy
4471 * @var WCF.Action.Proxy
4472 */
4473 _proxy: null,
4474
4475 /**
4476 * search input field
4477 * @var jQuery
4478 */
4479 _searchInput: null,
4480
4481 /**
4482 * minimum search input length, MUST be 1 or higher
4483 * @var integer
4484 */
4485 _triggerLength: 3,
4486
4487 /**
4488 * delay timer
4489 * @var WCF.PeriodicalExecuter
4490 */
4491 _timer: null,
4492
4493 /**
4494 * Initializes a new search.
4495 *
4496 * @param jQuery searchInput
4497 * @param object callback
4498 * @param array excludedSearchValues
4499 * @param boolean commaSeperated
4500 * @param boolean showLoadingOverlay
4501 */
4502 init: function(searchInput, callback, excludedSearchValues, commaSeperated, showLoadingOverlay) {
4503 if (callback !== null && callback !== undefined && !$.isFunction(callback)) {
4504 console.debug("[WCF.Search.Base] The given callback is invalid, aborting.");
4505 return;
4506 }
4507
4508 this._callback = (callback) ? callback : null;
4509 this._caretAt = -1;
4510 this._delay = 0;
4511 this._excludedSearchValues = [];
4512 if (excludedSearchValues) {
4513 this._excludedSearchValues = excludedSearchValues;
4514 }
4515
4516 this._searchInput = $(searchInput);
4517 if (!this._searchInput.length) {
4518 console.debug("[WCF.Search.Base] Selector '" + searchInput + "' for search input is invalid, aborting.");
4519 return;
4520 }
4521
4522 this._searchInput.keydown($.proxy(this._keyDown, this)).keyup($.proxy(this._keyUp, this)).wrap('<span class="dropdown" />');
4523
4524 if ($.browser.mozilla && $.browser.touch) {
4525 this._searchInput.on('input', $.proxy(this._keyUp, this));
4526 }
4527
4528 this._list = $('<ul class="dropdownMenu" />').insertAfter(this._searchInput);
4529 this._commaSeperated = (commaSeperated) ? true : false;
4530 this._oldSearchString = [ ];
4531
4532 this._itemCount = 0;
4533 this._itemIndex = -1;
4534
4535 this._proxy = new WCF.Action.Proxy({
4536 showLoadingOverlay: (showLoadingOverlay !== true ? false : true),
4537 success: $.proxy(this._success, this),
4538 autoAbortPrevious: true
4539 });
4540
4541 if (this._searchInput.is('input')) {
4542 this._searchInput.attr('autocomplete', 'off');
4543 }
4544
4545 this._searchInput.blur($.proxy(this._blur, this));
4546
4547 WCF.Dropdown.initDropdownFragment(this._searchInput.parent(), this._list);
4548 },
4549
4550 /**
4551 * Closes the dropdown after a short delay.
4552 */
4553 _blur: function() {
4554 var self = this;
4555 new WCF.PeriodicalExecuter(function(pe) {
4556 if (self._list.is(':visible')) {
4557 self._clearList(false);
4558 }
4559
4560 pe.stop();
4561 }, 250);
4562 },
4563
4564 /**
4565 * Blocks execution of 'Enter' event.
4566 *
4567 * @param object event
4568 */
4569 _keyDown: function(event) {
4570 if (event.which === $.ui.keyCode.ENTER) {
4571 var $dropdown = this._searchInput.parents('.dropdown');
4572
4573 if ($dropdown.data('disableAutoFocus')) {
4574 if (this._itemIndex !== -1) {
4575 event.preventDefault();
4576 }
4577 }
4578 else if ($dropdown.data('preventSubmit') || this._itemIndex !== -1) {
4579 event.preventDefault();
4580 }
4581 }
4582 },
4583
4584 /**
4585 * Performs a search upon key up.
4586 *
4587 * @param object event
4588 */
4589 _keyUp: function(event) {
4590 // handle arrow keys and return key
4591 switch (event.which) {
4592 case 37: // arrow-left
4593 case 39: // arrow-right
4594 return;
4595 break;
4596
4597 case 38: // arrow up
4598 this._selectPreviousItem();
4599 return;
4600 break;
4601
4602 case 40: // arrow down
4603 this._selectNextItem();
4604 return;
4605 break;
4606
4607 case 13: // return key
4608 return this._selectElement(event);
4609 break;
4610 }
4611
4612 var $content = this._getSearchString(event);
4613 if ($content === '') {
4614 this._clearList(false);
4615 }
4616 else if ($content.length >= this._triggerLength) {
4617 var $parameters = {
4618 data: {
4619 excludedSearchValues: this._excludedSearchValues,
4620 searchString: $content
4621 }
4622 };
4623
4624 if (this._delay) {
4625 if (this._timer !== null) {
4626 this._timer.stop();
4627 }
4628
4629 var self = this;
4630 this._timer = new WCF.PeriodicalExecuter(function() {
4631 self._queryServer($parameters);
4632
4633 self._timer.stop();
4634 self._timer = null;
4635 }, this._delay);
4636 }
4637 else {
4638 this._queryServer($parameters);
4639 }
4640 }
4641 else {
4642 // input below trigger length
4643 this._clearList(false);
4644 }
4645 },
4646
4647 /**
4648 * Queries the server.
4649 *
4650 * @param object parameters
4651 */
4652 _queryServer: function(parameters) {
4653 this._searchInput.parents('.searchBar').addClass('loading');
4654 this._proxy.setOption('data', {
4655 actionName: 'getSearchResultList',
4656 className: this._className,
4657 interfaceName: 'wcf\\data\\ISearchAction',
4658 parameters: this._getParameters(parameters)
4659 });
4660 this._proxy.sendRequest();
4661 },
4662
4663 /**
4664 * Sets query delay in miliseconds.
4665 *
4666 * @param integer delay
4667 */
4668 setDelay: function(delay) {
4669 this._delay = delay;
4670 },
4671
4672 /**
4673 * Selects the next item in list.
4674 */
4675 _selectNextItem: function() {
4676 if (this._itemCount === 0) {
4677 return;
4678 }
4679
4680 // remove previous marking
4681 this._itemIndex++;
4682 if (this._itemIndex === this._itemCount) {
4683 this._itemIndex = 0;
4684 }
4685
4686 this._highlightSelectedElement();
4687 },
4688
4689 /**
4690 * Selects the previous item in list.
4691 */
4692 _selectPreviousItem: function() {
4693 if (this._itemCount === 0) {
4694 return;
4695 }
4696
4697 this._itemIndex--;
4698 if (this._itemIndex === -1) {
4699 this._itemIndex = this._itemCount - 1;
4700 }
4701
4702 this._highlightSelectedElement();
4703 },
4704
4705 /**
4706 * Highlights the active item.
4707 */
4708 _highlightSelectedElement: function() {
4709 this._list.find('li').removeClass('dropdownNavigationItem');
4710 this._list.find('li:eq(' + this._itemIndex + ')').addClass('dropdownNavigationItem');
4711 },
4712
4713 /**
4714 * Selects the active item by pressing the return key.
4715 *
4716 * @param object event
4717 * @return boolean
4718 */
4719 _selectElement: function(event) {
4720 if (this._itemCount === 0) {
4721 return true;
4722 }
4723
4724 this._list.find('li.dropdownNavigationItem').trigger('click');
4725
4726 return false;
4727 },
4728
4729 /**
4730 * Returns search string.
4731 *
4732 * @return string
4733 */
4734 _getSearchString: function(event) {
4735 var $searchString = $.trim(this._searchInput.val());
4736 if (this._commaSeperated) {
4737 var $keyCode = event.keyCode || event.which;
4738 if ($keyCode == $.ui.keyCode.COMMA) {
4739 // ignore event if char is ','
4740 return '';
4741 }
4742
4743 var $current = $searchString.split(',');
4744 var $length = $current.length;
4745 for (var $i = 0; $i < $length; $i++) {
4746 // remove whitespaces at the beginning or end
4747 $current[$i] = $.trim($current[$i]);
4748 }
4749
4750 for (var $i = 0; $i < $length; $i++) {
4751 var $part = $current[$i];
4752
4753 if (this._oldSearchString[$i]) {
4754 // compare part
4755 if ($part != this._oldSearchString[$i]) {
4756 // current part was changed
4757 $searchString = $part;
4758 this._caretAt = $i;
4759
4760 break;
4761 }
4762 }
4763 else {
4764 // new part was added
4765 $searchString = $part;
4766 break;
4767 }
4768 }
4769
4770 this._oldSearchString = $current;
4771 }
4772
4773 return $searchString;
4774 },
4775
4776 /**
4777 * Returns parameters for quick search.
4778 *
4779 * @param object parameters
4780 * @return object
4781 */
4782 _getParameters: function(parameters) {
4783 return parameters;
4784 },
4785
4786 /**
4787 * Evalutes search results.
4788 *
4789 * @param object data
4790 * @param string textStatus
4791 * @param jQuery jqXHR
4792 */
4793 _success: function(data, textStatus, jqXHR) {
4794 this._clearList(false);
4795 this._searchInput.parents('.searchBar').removeClass('loading');
4796
4797 if ($.getLength(data.returnValues)) {
4798 for (var $i in data.returnValues) {
4799 var $item = data.returnValues[$i];
4800
4801 this._createListItem($item);
4802 }
4803 }
4804 else if (!this._handleEmptyResult()) {
4805 return;
4806 }
4807
4808 WCF.CloseOverlayHandler.addCallback('WCF.Search.Base', $.proxy(function() { this._clearList(); }, this));
4809
4810 var $containerID = this._searchInput.parents('.dropdown').wcfIdentify();
4811 if (!WCF.Dropdown.getDropdownMenu($containerID).hasClass('dropdownOpen')) {
4812 WCF.Dropdown.toggleDropdown($containerID);
4813 }
4814
4815 // pre-select first item
4816 this._itemIndex = -1;
4817 if (!WCF.Dropdown.getDropdown($containerID).data('disableAutoFocus')) {
4818 this._selectNextItem();
4819 }
4820 },
4821
4822 /**
4823 * Handles empty result lists, should return false if dropdown should be hidden.
4824 *
4825 * @return boolean
4826 */
4827 _handleEmptyResult: function() {
4828 return false;
4829 },
4830
4831 /**
4832 * Creates a new list item.
4833 *
4834 * @param object item
4835 * @return jQuery
4836 */
4837 _createListItem: function(item) {
4838 var $listItem = $('<li><span>' + WCF.String.escapeHTML(item.label) + '</span></li>').appendTo(this._list);
4839 $listItem.data('objectID', item.objectID).data('label', item.label).click($.proxy(this._executeCallback, this));
4840
4841 this._itemCount++;
4842
4843 return $listItem;
4844 },
4845
4846 /**
4847 * Executes callback upon result click.
4848 *
4849 * @param object event
4850 */
4851 _executeCallback: function(event) {
4852 var $clearSearchInput = false;
4853 var $listItem = $(event.currentTarget);
4854 // notify callback
4855 if (this._commaSeperated) {
4856 // auto-complete current part
4857 var $result = $listItem.data('label');
4858 this._oldSearchString[this._caretAt] = $result;
4859 this._searchInput.val(this._oldSearchString.join(', '));
4860
4861 if ($.browser.webkit) {
4862 // chrome won't display the new value until the textarea is rendered again
4863 // this quick fix forces chrome to render it again, even though it changes nothing
4864 this._searchInput.css({ display: 'block' });
4865 }
4866
4867 // set focus on input field again
4868 var $position = this._searchInput.val().toLowerCase().indexOf($result.toLowerCase()) + $result.length;
4869 this._searchInput.focus().setCaret($position);
4870 }
4871 else {
4872 if (this._callback === null) {
4873 this._searchInput.val($listItem.data('label'));
4874 }
4875 else {
4876 $clearSearchInput = (this._callback($listItem.data()) === true) ? true : false;
4877 }
4878 }
4879
4880 // close list and revert input
4881 this._clearList($clearSearchInput);
4882 },
4883
4884 /**
4885 * Closes the suggestion list and clears search input on demand.
4886 *
4887 * @param boolean clearSearchInput
4888 */
4889 _clearList: function(clearSearchInput) {
4890 if (clearSearchInput && !this._commaSeperated) {
4891 this._searchInput.val('');
4892 }
4893
4894 // close dropdown
4895 WCF.Dropdown.getDropdown(this._searchInput.parents('.dropdown').wcfIdentify()).removeClass('dropdownOpen');
4896 WCF.Dropdown.getDropdownMenu(this._searchInput.parents('.dropdown').wcfIdentify()).removeClass('dropdownOpen');
4897
4898 this._list.end().empty();
4899
4900 WCF.CloseOverlayHandler.removeCallback('WCF.Search.Base');
4901
4902 // reset item navigation
4903 this._itemCount = 0;
4904 this._itemIndex = -1;
4905 },
4906
4907 /**
4908 * Adds an excluded search value.
4909 *
4910 * @param string value
4911 */
4912 addExcludedSearchValue: function(value) {
4913 if (!WCF.inArray(value, this._excludedSearchValues)) {
4914 this._excludedSearchValues.push(value);
4915 }
4916 },
4917
4918 /**
4919 * Removes an excluded search value.
4920 *
4921 * @param string value
4922 */
4923 removeExcludedSearchValue: function(value) {
4924 var index = $.inArray(value, this._excludedSearchValues);
4925 if (index != -1) {
4926 this._excludedSearchValues.splice(index, 1);
4927 }
4928 }
4929 });
4930
4931 /**
4932 * Provides quick search for users and user groups.
4933 *
4934 * @see WCF.Search.Base
4935 * @deprecated 2.2 - please use `WoltLab/WCF/Ui/User/Search/Input` instead
4936 */
4937 WCF.Search.User = WCF.Search.Base.extend({
4938 /**
4939 * @see WCF.Search.Base._className
4940 */
4941 _className: 'wcf\\data\\user\\UserAction',
4942
4943 /**
4944 * include user groups in search
4945 * @var boolean
4946 */
4947 _includeUserGroups: false,
4948
4949 /**
4950 * @see WCF.Search.Base.init()
4951 */
4952 init: function(searchInput, callback, includeUserGroups, excludedSearchValues, commaSeperated) {
4953 this._includeUserGroups = includeUserGroups;
4954
4955 this._super(searchInput, callback, excludedSearchValues, commaSeperated);
4956 },
4957
4958 /**
4959 * @see WCF.Search.Base._getParameters()
4960 */
4961 _getParameters: function(parameters) {
4962 parameters.data.includeUserGroups = this._includeUserGroups ? 1 : 0;
4963
4964 return parameters;
4965 },
4966
4967 /**
4968 * @see WCF.Search.Base._createListItem()
4969 */
4970 _createListItem: function(item) {
4971 var $listItem = this._super(item);
4972
4973 var $icon = null;
4974 if (item.icon) {
4975 $icon = $(item.icon);
4976 }
4977 else if (this._includeUserGroups && item.type === 'group') {
4978 $icon = $('<span class="icon icon16 fa-users" />');
4979 }
4980
4981 if ($icon) {
4982 var $label = $listItem.find('span').detach();
4983
4984 var $box16 = $('<div />').addClass('box16').appendTo($listItem);
4985
4986 $box16.append($icon);
4987 $box16.append($('<div />').append($label));
4988 }
4989
4990 // insert item type
4991 $listItem.data('type', item.type);
4992
4993 return $listItem;
4994 }
4995 });
4996
4997 /**
4998 * Namespace for system-related classes.
4999 */
5000 WCF.System = { };
5001
5002 /**
5003 * Namespace for dependency-related classes.
5004 */
5005 WCF.System.Dependency = { };
5006
5007 /**
5008 * JavaScript Dependency Manager.
5009 */
5010 WCF.System.Dependency.Manager = {
5011 /**
5012 * list of callbacks grouped by identifier
5013 * @var object
5014 */
5015 _callbacks: { },
5016
5017 /**
5018 * list of loaded identifiers
5019 * @var array<string>
5020 */
5021 _loaded: [ ],
5022
5023 /**
5024 * list of setup callbacks grouped by identifier
5025 * @var object
5026 */
5027 _setupCallbacks: { },
5028
5029 /**
5030 * Registers a callback for given identifier, will be executed after all setup
5031 * callbacks have been invoked.
5032 *
5033 * @param string identifier
5034 * @param object callback
5035 */
5036 register: function(identifier, callback) {
5037 if (!$.isFunction(callback)) {
5038 console.debug("[WCF.System.Dependency.Manager] Callback for identifier '" + identifier + "' is invalid, aborting.");
5039 return;
5040 }
5041
5042 // already loaded, invoke now
5043 if (WCF.inArray(identifier, this._loaded)) {
5044 setTimeout(function() {
5045 callback();
5046 }, 1);
5047 }
5048 else {
5049 if (!this._callbacks[identifier]) {
5050 this._callbacks[identifier] = [ ];
5051 }
5052
5053 this._callbacks[identifier].push(callback);
5054 }
5055 },
5056
5057 /**
5058 * Registers a setup callback for given identifier, will be invoked
5059 * prior to all other callbacks.
5060 *
5061 * @param string identifier
5062 * @param object callback
5063 */
5064 setup: function(identifier, callback) {
5065 if (!$.isFunction(callback)) {
5066 console.debug("[WCF.System.Dependency.Manager] Setup callback for identifier '" + identifier + "' is invalid, aborting.");
5067 return;
5068 }
5069
5070 if (!this._setupCallbacks[identifier]) {
5071 this._setupCallbacks[identifier] = [ ];
5072 }
5073
5074 this._setupCallbacks[identifier].push(callback);
5075 },
5076
5077 /**
5078 * Invokes all callbacks for given identifier and marks it as loaded.
5079 *
5080 * @param string identifier
5081 */
5082 invoke: function(identifier) {
5083 if (this._setupCallbacks[identifier]) {
5084 for (var $i = 0, $length = this._setupCallbacks[identifier].length; $i < $length; $i++) {
5085 this._setupCallbacks[identifier][$i]();
5086 }
5087
5088 delete this._setupCallbacks[identifier];
5089 }
5090
5091 this._loaded.push(identifier);
5092
5093 if (this._callbacks[identifier]) {
5094 for (var $i = 0, $length = this._callbacks[identifier].length; $i < $length; $i++) {
5095 this._callbacks[identifier][$i]();
5096 }
5097
5098 delete this._callbacks[identifier];
5099 }
5100 },
5101
5102 reset: function(identifier) {
5103 var index = this._loaded.indexOf(identifier);
5104 if (index !== -1) {
5105 this._loaded.splice(index, 1);
5106 }
5107 }
5108 };
5109
5110 /**
5111 * Provides flexible dropdowns for tab-based menus.
5112 */
5113 WCF.System.FlexibleMenu = {
5114 /**
5115 * Initializes the WCF.System.FlexibleMenu class.
5116 */
5117 init: function() { /* does nothing */ },
5118
5119 /**
5120 * Registers a tab-based menu by id.
5121 *
5122 * Required DOM:
5123 * <container>
5124 * <ul style="white-space: nowrap">
5125 * <li>tab 1</li>
5126 * <li>tab 2</li>
5127 * ...
5128 * <li>tab n</li>
5129 * </ul>
5130 * </container>
5131 *
5132 * @param string containerID
5133 */
5134 registerMenu: function(containerID) {
5135 require(['WoltLab/WCF/Ui/FlexibleMenu'], function(UiFlexibleMenu) {
5136 UiFlexibleMenu.register(containerID);
5137 });
5138 },
5139
5140 /**
5141 * Rebuilds a container, will be automatically invoked on window resize and registering.
5142 *
5143 * @param string containerID
5144 */
5145 rebuild: function(containerID) {
5146 require(['WoltLab/WCF/Ui/FlexibleMenu'], function(UiFlexibleMenu) {
5147 UiFlexibleMenu.rebuild(containerID);
5148 });
5149 }
5150 };
5151
5152 /**
5153 * Namespace for mobile device-related classes.
5154 */
5155 WCF.System.Mobile = { };
5156
5157 /**
5158 * Stores object references for global access.
5159 */
5160 WCF.System.ObjectStore = {
5161 /**
5162 * list of objects grouped by identifier
5163 * @var object<array>
5164 */
5165 _objects: { },
5166
5167 /**
5168 * Adds a new object to the collection.
5169 *
5170 * @param string identifier
5171 * @param object object
5172 */
5173 add: function(identifier, obj) {
5174 if (this._objects[identifier] === undefined) {
5175 this._objects[identifier] = [ ];
5176 }
5177
5178 this._objects[identifier].push(obj);
5179 },
5180
5181 /**
5182 * Invokes a callback passing the matching objects as a parameter.
5183 *
5184 * @param string identifier
5185 * @param object callback
5186 */
5187 invoke: function(identifier, callback) {
5188 if (this._objects[identifier]) {
5189 for (var $i = 0; $i < this._objects[identifier].length; $i++) {
5190 callback(this._objects[identifier][$i]);
5191 }
5192 }
5193 }
5194 };
5195
5196 /**
5197 * Stores captcha callbacks used for captchas in AJAX contexts.
5198 */
5199 WCF.System.Captcha = {
5200 /**
5201 * adds call
5202 * @var object<function>
5203 */
5204 _captchas: { },
5205
5206 /**
5207 * Adds a callback for a certain captcha.
5208 *
5209 * @param string captchaID
5210 * @param function callback
5211 */
5212 addCallback: function(captchaID, callback) {
5213 if (!$.isFunction(callback)) {
5214 console.debug('[WCF.System.Captcha] Given callback is no function');
5215 return;
5216 }
5217
5218 this._captchas[captchaID] = callback;
5219 },
5220
5221 /**
5222 * Returns the captcha data for the captcha with the given id.
5223 *
5224 * @return object
5225 */
5226 getData: function(captchaID) {
5227 if (this._captchas[captchaID] === undefined) {
5228 console.debug('[WCF.System.Captcha] Unknow captcha id "' + captchaID + '"');
5229 return;
5230 }
5231
5232 return this._captchas[captchaID]();
5233 },
5234
5235 /**
5236 * Removes the callback with the given captcha id.
5237 */
5238 removeCallback: function(captchaID) {
5239 delete this._captchas[captchaID];
5240 }
5241 };
5242
5243 WCF.System.Page = { };
5244
5245 /**
5246 * System notification overlays.
5247 *
5248 * @deprecated 2.2 - please use `Ui/Notification` instead
5249 *
5250 * @param string message
5251 * @param string cssClassNames
5252 */
5253 WCF.System.Notification = Class.extend({
5254 _cssClassNames: '',
5255 _message: '',
5256
5257 /**
5258 * Creates a new system notification overlay.
5259 *
5260 * @param string message
5261 * @param string cssClassNames
5262 */
5263 init: function(message, cssClassNames) {
5264 this._cssClassNames = cssClassNames || '';
5265 this._message = message || '';
5266 },
5267
5268 /**
5269 * Shows the notification overlay.
5270 *
5271 * @param object callback
5272 * @param integer duration
5273 * @param string message
5274 * @param string cssClassName
5275 */
5276 show: function(callback, duration, message, cssClassNames) {
5277 require(['Ui/Notification'], (function(UiNotification) {
5278 UiNotification.show(
5279 message || this._message,
5280 callback,
5281 cssClassNames || this._cssClassNames
5282 );
5283 }).bind(this));
5284 }
5285 });
5286
5287 /**
5288 * Provides dialog-based confirmations.
5289 *
5290 * @deprecated 2.2 - please use `Ui/Confirmation` instead
5291 */
5292 WCF.System.Confirmation = {
5293 /**
5294 * Displays a confirmation dialog.
5295 *
5296 * @param string message
5297 * @param object callback
5298 * @param object parameters
5299 * @param jQuery template
5300 */
5301 show: function(message, callback, parameters, template) {
5302 require(['Ui/Confirmation'], function(UiConfirmation) {
5303 UiConfirmation.show({
5304 legacyCallback: callback,
5305 message: message,
5306 parameters: parameters,
5307 template: (typeof template === 'object' ? template.html() : '')
5308 });
5309 });
5310 }
5311 };
5312
5313 /**
5314 * Disables the ability to scroll the page.
5315 */
5316 WCF.System.DisableScrolling = {
5317 /**
5318 * number of times scrolling was disabled (nested calls)
5319 * @var integer
5320 */
5321 _depth: 0,
5322
5323 /**
5324 * old overflow-value of the body element
5325 * @var string
5326 */
5327 _oldOverflow: null,
5328
5329 /**
5330 * Disables scrolling.
5331 */
5332 disable: function () {
5333 // do not block scrolling on touch devices
5334 if ($.browser.touch) {
5335 return;
5336 }
5337
5338 if (this._depth === 0) {
5339 this._oldOverflow = $(document.body).css('overflow');
5340 $(document.body).css('overflow', 'hidden');
5341 }
5342
5343 this._depth++;
5344 },
5345
5346 /**
5347 * Enables scrolling again.
5348 * Must be called the same number of times disable() was called to enable scrolling.
5349 */
5350 enable: function () {
5351 if (this._depth === 0) return;
5352
5353 this._depth--;
5354
5355 if (this._depth === 0) {
5356 $(document.body).css('overflow', this._oldOverflow);
5357 }
5358 }
5359 };
5360
5361 /**
5362 * Disables the ability to zoom the page.
5363 */
5364 WCF.System.DisableZoom = {
5365 /**
5366 * number of times zoom was disabled (nested calls)
5367 * @var integer
5368 */
5369 _depth: 0,
5370
5371 /**
5372 * old viewport settings in meta[name=viewport]
5373 * @var string
5374 */
5375 _oldViewportSettings: null,
5376
5377 /**
5378 * Disables zooming.
5379 */
5380 disable: function () {
5381 if (this._depth === 0) {
5382 var $meta = $('meta[name=viewport]');
5383 this._oldViewportSettings = $meta.attr('content');
5384 $meta.attr('content', this._oldViewportSettings + ',maximum-scale=1');
5385 }
5386
5387 this._depth++;
5388 },
5389
5390 /**
5391 * Enables scrolling again.
5392 * Must be called the same number of times disable() was called to enable scrolling.
5393 */
5394 enable: function () {
5395 if (this._depth === 0) return;
5396
5397 this._depth--;
5398
5399 if (this._depth === 0) {
5400 $('meta[name=viewport]').attr('content', this._oldViewportSettings);
5401 }
5402 }
5403 };
5404
5405 /**
5406 * Puts an element into HTML 5 fullscreen mode.
5407 */
5408 WCF.System.Fullscreen = {
5409 /**
5410 * Puts the given element into full screen mode.
5411 * Note: This must be a raw HTMLElement, not a jQuery wrapped one.
5412 * Note: This must be called from an user triggered event listener for
5413 * security reasons.
5414 *
5415 * @param object Element to show full screen.
5416 */
5417 enterFullscreen: function (element) {
5418 if (element.requestFullscreen) {
5419 element.requestFullscreen();
5420 }
5421 else if (element.msRequestFullscreen) {
5422 element.msRequestFullscreen();
5423 }
5424 else if (element.mozRequestFullScreen) {
5425 element.mozRequestFullScreen();
5426 }
5427 else if (element.webkitRequestFullscreen) {
5428 element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
5429 }
5430 },
5431 /**
5432 * Toggles full screen mode. Either calls `enterFullscreen` with the given
5433 * element, if full screen mode is not active. Calls `exitFullscreen`
5434 * otherwise.
5435 */
5436 toggleFullscreen: function (element) {
5437 if (this.getFullscreenElement() === null) {
5438 this.enterFullscreen(element);
5439 }
5440 else {
5441 this.exitFullscreen();
5442 }
5443 },
5444 /**
5445 * Retrieves the element that is shown in full screen mode.
5446 * Returns null if either full screen mode is not supported or
5447 * if full screen mode is not active.
5448 *
5449 * @return object
5450 */
5451 getFullscreenElement: function () {
5452 if (document.fullscreenElement) {
5453 return document.fullscreenElement;
5454 }
5455 else if (document.mozFullScreenElement) {
5456 return document.mozFullScreenElement;
5457 }
5458 else if (document.webkitFullscreenElement) {
5459 return document.webkitFullscreenElement;
5460 }
5461 else if (document.msFullscreenElement) {
5462 return document.msFullscreenElement;
5463 }
5464
5465 return null;
5466 },
5467 /**
5468 * Exits full screen mode.
5469 */
5470 exitFullscreen: function () {
5471 if (document.exitFullscreen) {
5472 document.exitFullscreen();
5473 }
5474 else if (document.msExitFullscreen) {
5475 document.msExitFullscreen();
5476 }
5477 else if (document.mozCancelFullScreen) {
5478 document.mozCancelFullScreen();
5479 }
5480 else if (document.webkitExitFullscreen) {
5481 document.webkitExitFullscreen();
5482 }
5483 },
5484 /**
5485 * Returns whether the full screen API is supported in this browser.
5486 *
5487 * @return boolean
5488 */
5489 isSupported: function () {
5490 if (document.documentElement.requestFullscreen || document.documentElement.msRequestFullscreen || document.documentElement.mozRequestFullScreen || document.documentElement.webkitRequestFullscreen) {
5491 return true;
5492 }
5493
5494 return false;
5495 }
5496 };
5497
5498 /**
5499 * Provides the 'jump to page' overlay.
5500 *
5501 * @deprecated 2.2 - use `WoltLab/WCF/Ui/Page/JumpTo` instead
5502 */
5503 WCF.System.PageNavigation = {
5504 init: function(selector, callback) {
5505 require(['WoltLab/WCF/Ui/Page/JumpTo'], function(UiPageJumpTo) {
5506 var elements = elBySelAll(selector);
5507 for (var i = 0, length = elements.length; i < length; i++) {
5508 UiPageJumpTo.init(elements[i], callback);
5509 }
5510 });
5511 }
5512 };
5513
5514 /**
5515 * Sends periodical requests to protect the session from expiring. By default
5516 * it will send a request 1 minute before it would expire.
5517 *
5518 * @param integer seconds
5519 */
5520 WCF.System.KeepAlive = Class.extend({
5521 /**
5522 * Initializes the WCF.System.KeepAlive class.
5523 *
5524 * @param integer seconds
5525 */
5526 init: function(seconds) {
5527 new WCF.PeriodicalExecuter(function(pe) {
5528 new WCF.Action.Proxy({
5529 autoSend: true,
5530 data: {
5531 actionName: 'keepAlive',
5532 className: 'wcf\\data\\session\\SessionAction'
5533 },
5534 failure: function() { pe.stop(); },
5535 showLoadingOverlay: false,
5536 success: function(data) {
5537 WCF.System.PushNotification.executeCallbacks(data);
5538 },
5539 suppressErrors: true
5540 });
5541 }, (seconds * 1000));
5542 }
5543 });
5544
5545 /**
5546 * System-wide handler for push notifications.
5547 */
5548 WCF.System.PushNotification = {
5549 /**
5550 * list of callbacks groupped by type
5551 * @var object<array>
5552 */
5553 _callbacks: { },
5554
5555 /**
5556 * Adds a callback for a specific notification type.
5557 *
5558 * @param string type
5559 * @param object callback
5560 */
5561 addCallback: function(type, callback) {
5562 if (this._callbacks[type] === undefined) {
5563 this._callbacks[type] = [ ];
5564 }
5565
5566 this._callbacks[type].push(callback);
5567 },
5568
5569 /**
5570 * Executes all registered callbacks by type.
5571 *
5572 * @param object data
5573 */
5574 executeCallbacks: function(data) {
5575 for (var $type in data.returnValues) {
5576 if (this._callbacks[$type] !== undefined) {
5577 for (var $i = 0; $i < this._callbacks[$type].length; $i++) {
5578 this._callbacks[$type][$i](data.returnValues[$type]);
5579 }
5580 }
5581 }
5582 }
5583 };
5584
5585 /**
5586 * System-wide event system.
5587 *
5588 * @deprecated 2.2 - please use `EventHandler` instead
5589 */
5590 WCF.System.Event = {
5591 /**
5592 * Registers a new event listener.
5593 *
5594 * @param string identifier
5595 * @param string action
5596 * @param object listener
5597 * @return string
5598 */
5599 addListener: function(identifier, action, listener) {
5600 return window.__wcf_bc_eventHandler.add(identifier, action, listener);
5601 },
5602
5603 /**
5604 * Removes a listener, requires the uuid returned by addListener().
5605 *
5606 * @param string identifier
5607 * @param string action
5608 * @param string uuid
5609 * @return boolean
5610 */
5611 removeListener: function(identifier, action, uuid) {
5612 return window.__wcf_bc_eventHandler.remove(identifier, action, uuid);
5613 },
5614
5615 /**
5616 * Removes all registered event listeners for given identifier and action.
5617 *
5618 * @param string identifier
5619 * @param string action
5620 * @return boolean
5621 */
5622 removeAllListeners: function(identifier, action) {
5623 return window.__wcf_bc_eventHandler.removeAll(identifier, action);
5624 },
5625
5626 /**
5627 * Fires a new event and notifies all registered event listeners.
5628 *
5629 * @param string identifier
5630 * @param string action
5631 * @param object data
5632 */
5633 fireEvent: function(identifier, action, data) {
5634 window.__wcf_bc_eventHandler.fire(identifier, action, data);
5635 }
5636 };
5637
5638 /**
5639 * Worker support for frontend based upon DatabaseObjectActions.
5640 *
5641 * @param string className
5642 * @param string title
5643 * @param object parameters
5644 * @param object callback
5645 */
5646 WCF.System.Worker = Class.extend({
5647 /**
5648 * worker aborted
5649 * @var boolean
5650 */
5651 _aborted: false,
5652
5653 /**
5654 * DBOAction method name
5655 * @var string
5656 */
5657 _actionName: '',
5658
5659 /**
5660 * callback invoked after worker completed
5661 * @var object
5662 */
5663 _callback: null,
5664
5665 /**
5666 * DBOAction class name
5667 * @var string
5668 */
5669 _className: '',
5670
5671 /**
5672 * dialog object
5673 * @var jQuery
5674 */
5675 _dialog: null,
5676
5677 /**
5678 * action proxy
5679 * @var WCF.Action.Proxy
5680 */
5681 _proxy: null,
5682
5683 /**
5684 * dialog title
5685 * @var string
5686 */
5687 _title: '',
5688
5689 /**
5690 * Initializes a new worker instance.
5691 *
5692 * @param string actionName
5693 * @param string className
5694 * @param string title
5695 * @param object parameters
5696 * @param object callback
5697 * @param object confirmMessage
5698 */
5699 init: function(actionName, className, title, parameters, callback) {
5700 this._aborted = false;
5701 this._actionName = actionName;
5702 this._callback = callback || null;
5703 this._className = className;
5704 this._dialog = null;
5705 this._proxy = new WCF.Action.Proxy({
5706 autoSend: true,
5707 data: {
5708 actionName: this._actionName,
5709 className: this._className,
5710 parameters: parameters || { }
5711 },
5712 showLoadingOverlay: false,
5713 success: $.proxy(this._success, this)
5714 });
5715 this._title = title;
5716 },
5717
5718 /**
5719 * Handles response from server.
5720 *
5721 * @param object data
5722 */
5723 _success: function(data) {
5724 // init binding
5725 if (this._dialog === null) {
5726 this._dialog = $('<div />').hide().appendTo(document.body);
5727 this._dialog.wcfDialog({
5728 closeConfirmMessage: WCF.Language.get('wcf.worker.abort.confirmMessage'),
5729 closeViaModal: false,
5730 onClose: $.proxy(function() {
5731 this._aborted = true;
5732 this._proxy.abortPrevious();
5733
5734 window.location.reload();
5735 }, this),
5736 title: this._title
5737 });
5738 }
5739
5740 if (this._aborted) {
5741 return;
5742 }
5743
5744 if (data.returnValues.template) {
5745 this._dialog.html(data.returnValues.template);
5746 }
5747
5748 // update progress
5749 this._dialog.find('progress').attr('value', data.returnValues.progress).text(data.returnValues.progress + '%').next('span').text(data.returnValues.progress + '%');
5750
5751 // worker is still busy with its business, carry on
5752 if (data.returnValues.progress < 100) {
5753 // send request for next loop
5754 var $parameters = data.returnValues.parameters || { };
5755 $parameters.loopCount = data.returnValues.loopCount;
5756
5757 this._proxy.setOption('data', {
5758 actionName: this._actionName,
5759 className: this._className,
5760 parameters: $parameters
5761 });
5762 this._proxy.sendRequest();
5763 }
5764 else if (this._callback !== null) {
5765 this._callback(this, data);
5766 }
5767 else {
5768 // exchange icon
5769 this._dialog.find('.fa-spinner').removeClass('fa-spinner').addClass('fa-check green');
5770 this._dialog.find('.contentHeader h1').text(WCF.Language.get('wcf.global.worker.completed'));
5771
5772 // display continue button
5773 var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog);
5774 $('<button class="buttonPrimary">' + WCF.Language.get('wcf.global.button.next') + '</button>').appendTo($formSubmit).focus().click(function() {
5775 if (data.returnValues.redirectURL) {
5776 window.location = data.returnValues.redirectURL;
5777 }
5778 else {
5779 window.location.reload();
5780 }
5781 });
5782
5783 this._dialog.wcfDialog('render');
5784 }
5785 }
5786 });
5787
5788 /**
5789 * Default implementation for inline editors.
5790 *
5791 * @param string elementSelector
5792 */
5793 WCF.InlineEditor = Class.extend({
5794 /**
5795 * list of registered callbacks
5796 * @var array<object>
5797 */
5798 _callbacks: [ ],
5799
5800 /**
5801 * list of dropdown selections
5802 * @var object
5803 */
5804 _dropdowns: { },
5805
5806 /**
5807 * list of container elements
5808 * @var object
5809 */
5810 _elements: { },
5811
5812 /**
5813 * notification object
5814 * @var WCF.System.Notification
5815 */
5816 _notification: null,
5817
5818 /**
5819 * list of known options
5820 * @var array<object>
5821 */
5822 _options: [ ],
5823
5824 /**
5825 * action proxy
5826 * @var WCF.Action.Proxy
5827 */
5828 _proxy: null,
5829
5830 /**
5831 * list of trigger elements by element id
5832 * @var object<object>
5833 */
5834 _triggerElements: { },
5835
5836 /**
5837 * list of data to update upon success
5838 * @var array<object>
5839 */
5840 _updateData: [ ],
5841
5842 /**
5843 * Initializes a new inline editor.
5844 */
5845 init: function(elementSelector) {
5846 var $elements = $(elementSelector);
5847 if (!$elements.length) {
5848 return;
5849 }
5850
5851 this._setOptions();
5852 var $quickOption = '';
5853 for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
5854 if (this._options[$i].isQuickOption) {
5855 $quickOption = this._options[$i].optionName;
5856 break;
5857 }
5858 }
5859
5860 var self = this;
5861 $elements.each(function(index, element) {
5862 var $element = $(element);
5863 var $elementID = $element.wcfIdentify();
5864
5865 // find trigger element
5866 var $trigger = self._getTriggerElement($element);
5867 if ($trigger === null || $trigger.length !== 1) {
5868 return;
5869 }
5870
5871 $trigger.click($.proxy(self._show, self)).data('elementID', $elementID);
5872 if ($quickOption) {
5873 // simulate click on target action
5874 $trigger.disableSelection().data('optionName', $quickOption).dblclick($.proxy(self._click, self));
5875 }
5876
5877 // store reference
5878 self._elements[$elementID] = $element;
5879 });
5880
5881 this._proxy = new WCF.Action.Proxy({
5882 success: $.proxy(this._success, this)
5883 });
5884
5885 WCF.CloseOverlayHandler.addCallback('WCF.InlineEditor', $.proxy(this._closeAll, this));
5886
5887 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success');
5888 },
5889
5890 /**
5891 * Closes all inline editors.
5892 */
5893 _closeAll: function() {
5894 for (var $elementID in this._elements) {
5895 this._hide($elementID);
5896 }
5897 },
5898
5899 /**
5900 * Sets options for this inline editor.
5901 */
5902 _setOptions: function() {
5903 this._options = [ ];
5904 },
5905
5906 /**
5907 * Register an option callback for validation and execution.
5908 *
5909 * @param object callback
5910 */
5911 registerCallback: function(callback) {
5912 if ($.isFunction(callback)) {
5913 this._callbacks.push(callback);
5914 }
5915 },
5916
5917 /**
5918 * Returns the triggering element.
5919 *
5920 * @param jQuery element
5921 * @return jQuery
5922 */
5923 _getTriggerElement: function(element) {
5924 return null;
5925 },
5926
5927 /**
5928 * Shows a dropdown menu if options are available.
5929 *
5930 * @param object event
5931 */
5932 _show: function(event) {
5933 event.preventDefault();
5934 var $elementID = $(event.currentTarget).data('elementID');
5935
5936 // build dropdown
5937 var $trigger = null;
5938 if (!this._dropdowns[$elementID]) {
5939 this._triggerElements[$elementID] = $trigger = this._getTriggerElement(this._elements[$elementID]).addClass('dropdownToggle');
5940 var parent = $trigger[0].parentNode;
5941 if (parent && parent.nodeName === 'LI' && parent.childElementCount === 1) {
5942 // do not add a wrapper element if the trigger is the only child
5943 parent.classList.add('dropdown');
5944 }
5945 else {
5946 $trigger.wrap('<span class="dropdown" />');
5947 }
5948
5949 this._dropdowns[$elementID] = $('<ul class="dropdownMenu" />').insertAfter($trigger);
5950 }
5951 this._dropdowns[$elementID].empty();
5952
5953 // validate options
5954 var $hasOptions = false;
5955 var $lastElementType = '';
5956 for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
5957 var $option = this._options[$i];
5958
5959 if ($option.optionName === 'divider') {
5960 if ($lastElementType !== '' && $lastElementType !== 'divider') {
5961 $('<li class="dropdownDivider" />').appendTo(this._dropdowns[$elementID]);
5962 $lastElementType = $option.optionName;
5963 }
5964 }
5965 else if (this._validate($elementID, $option.optionName) || this._validateCallbacks($elementID, $option.optionName)) {
5966 var $listItem = $('<li><span>' + $option.label + '</span></li>').appendTo(this._dropdowns[$elementID]);
5967 $listItem.data('elementID', $elementID).data('optionName', $option.optionName).data('isQuickOption', ($option.isQuickOption ? true : false)).click($.proxy(this._click, this));
5968
5969 $hasOptions = true;
5970 $lastElementType = $option.optionName;
5971 }
5972 }
5973
5974 if ($hasOptions) {
5975 // if last child is divider, remove it
5976 var $lastChild = this._dropdowns[$elementID].children().last();
5977 if ($lastChild.hasClass('dropdownDivider')) {
5978 $lastChild.remove();
5979 }
5980
5981 // check if only element is a quick option
5982 var $quickOption = null;
5983 var $count = 0;
5984 this._dropdowns[$elementID].children().each(function(index, child) {
5985 var $child = $(child);
5986 if (!$child.hasClass('dropdownDivider')) {
5987 if ($child.data('isQuickOption')) {
5988 $quickOption = $child;
5989 }
5990 else {
5991 $count++;
5992 }
5993 }
5994 });
5995
5996 if (!$count) {
5997 $quickOption.trigger('click');
5998
5999 if (this._triggerElements[$elementID]) {
6000 WCF.Dropdown.close(this._triggerElements[$elementID].parents('.dropdown').wcfIdentify());
6001 }
6002
6003 return false;
6004 }
6005 }
6006
6007 if ($trigger !== null) {
6008 WCF.Dropdown.initDropdown($trigger, true);
6009 }
6010
6011 return false;
6012 },
6013
6014 /**
6015 * Validates an option.
6016 *
6017 * @param string elementID
6018 * @param string optionName
6019 * @returns boolean
6020 */
6021 _validate: function(elementID, optionName) {
6022 return false;
6023 },
6024
6025 /**
6026 * Validates an option provided by callbacks.
6027 *
6028 * @param string elementID
6029 * @param string optionName
6030 * @return boolean
6031 */
6032 _validateCallbacks: function(elementID, optionName) {
6033 var $length = this._callbacks.length;
6034 if ($length) {
6035 for (var $i = 0; $i < $length; $i++) {
6036 if (this._callbacks[$i].validate(this._elements[elementID], optionName)) {
6037 return true;
6038 }
6039 }
6040 }
6041
6042 return false;
6043 },
6044
6045 /**
6046 * Handles AJAX responses.
6047 *
6048 * @param object data
6049 * @param string textStatus
6050 * @param jQuery jqXHR
6051 */
6052 _success: function(data, textStatus, jqXHR) {
6053 var $length = this._updateData.length;
6054 if (!$length) {
6055 return;
6056 }
6057
6058 this._updateState(data);
6059
6060 this._updateData = [ ];
6061 },
6062
6063 /**
6064 * Update element states based upon update data.
6065 *
6066 * @param object data
6067 */
6068 _updateState: function(data) { },
6069
6070 /**
6071 * Handles clicks within dropdown.
6072 *
6073 * @param object event
6074 */
6075 _click: function(event) {
6076 var $listItem = $(event.currentTarget);
6077 var $elementID = $listItem.data('elementID');
6078 var $optionName = $listItem.data('optionName');
6079
6080 if (!this._execute($elementID, $optionName)) {
6081 this._executeCallback($elementID, $optionName);
6082 }
6083
6084 this._hide($elementID);
6085 },
6086
6087 /**
6088 * Executes actions associated with an option.
6089 *
6090 * @param string elementID
6091 * @param string optionName
6092 * @return boolean
6093 */
6094 _execute: function(elementID, optionName) {
6095 return false;
6096 },
6097
6098 /**
6099 * Executes actions associated with an option provided by callbacks.
6100 *
6101 * @param string elementID
6102 * @param string optionName
6103 * @return boolean
6104 */
6105 _executeCallback: function(elementID, optionName) {
6106 var $length = this._callbacks.length;
6107 if ($length) {
6108 for (var $i = 0; $i < $length; $i++) {
6109 if (this._callbacks[$i].execute(this._elements[elementID], optionName)) {
6110 return true;
6111 }
6112 }
6113 }
6114
6115 return false;
6116 },
6117
6118 /**
6119 * Hides a dropdown menu.
6120 *
6121 * @param string elementID
6122 */
6123 _hide: function(elementID) {
6124 if (this._dropdowns[elementID]) {
6125 this._dropdowns[elementID].empty().removeClass('dropdownOpen');
6126 }
6127 }
6128 });
6129
6130 /**
6131 * Default implementation for ajax file uploads.
6132 *
6133 * @deprecated Use WoltLab/WCF/Upload
6134 *
6135 * @param jquery buttonSelector
6136 * @param jquery fileListSelector
6137 * @param string className
6138 * @param jquery options
6139 */
6140 WCF.Upload = Class.extend({
6141 /**
6142 * name of the upload field
6143 * @var string
6144 */
6145 _name: '__files[]',
6146
6147 /**
6148 * button selector
6149 * @var jQuery
6150 */
6151 _buttonSelector: null,
6152
6153 /**
6154 * file list selector
6155 * @var jQuery
6156 */
6157 _fileListSelector: null,
6158
6159 /**
6160 * upload file
6161 * @var jQuery
6162 */
6163 _fileUpload: null,
6164
6165 /**
6166 * class name
6167 * @var string
6168 */
6169 _className: '',
6170
6171 /**
6172 * iframe for IE<10 fallback
6173 * @var jQuery
6174 */
6175 _iframe: null,
6176
6177 /**
6178 * internal file id
6179 * @var integer
6180 */
6181 _internalFileID: 0,
6182
6183 /**
6184 * additional options
6185 * @var jQuery
6186 */
6187 _options: {},
6188
6189 /**
6190 * upload matrix
6191 * @var array
6192 */
6193 _uploadMatrix: [],
6194
6195 /**
6196 * true, if the active user's browser supports ajax file uploads
6197 * @var boolean
6198 */
6199 _supportsAJAXUpload: true,
6200
6201 /**
6202 * fallback overlay for stupid browsers
6203 * @var jquery
6204 */
6205 _overlay: null,
6206
6207 /**
6208 * Initializes a new upload handler.
6209 *
6210 * @param string buttonSelector
6211 * @param string fileListSelector
6212 * @param string className
6213 * @param object options
6214 */
6215 init: function(buttonSelector, fileListSelector, className, options) {
6216 this._buttonSelector = buttonSelector;
6217 this._fileListSelector = fileListSelector;
6218 this._className = className;
6219 this._internalFileID = 0;
6220 this._options = $.extend(true, {
6221 action: 'upload',
6222 multiple: false,
6223 url: 'index.php/AJAXUpload/?t=' + SECURITY_TOKEN + SID_ARG_2ND
6224 }, options || { });
6225
6226 this._options.url = WCF.convertLegacyURL(this._options.url);
6227
6228 // check for ajax upload support
6229 var $xhr = new XMLHttpRequest();
6230 this._supportsAJAXUpload = ($xhr && ('upload' in $xhr) && ('onprogress' in $xhr.upload));
6231
6232 // create upload button
6233 this._createButton();
6234 },
6235
6236 /**
6237 * Creates the upload button.
6238 */
6239 _createButton: function() {
6240 if (this._supportsAJAXUpload) {
6241 this._fileUpload = $('<input type="file" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/>');
6242 this._fileUpload.change($.proxy(this._upload, this));
6243 var $button = $('<p class="button uploadButton"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>');
6244 $button.prepend(this._fileUpload);
6245 }
6246 else {
6247 var $button = $('<p class="button uploadFallbackButton"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>');
6248 $button.click($.proxy(this._showOverlay, this));
6249 }
6250
6251 this._insertButton($button);
6252 },
6253
6254 /**
6255 * Inserts the upload button.
6256 *
6257 * @param jQuery button
6258 */
6259 _insertButton: function(button) {
6260 this._buttonSelector.prepend(button);
6261 },
6262
6263 /**
6264 * Removes the upload button.
6265 */
6266 _removeButton: function() {
6267 var $selector = '.uploadButton';
6268 if (!this._supportsAJAXUpload) {
6269 $selector = '.uploadFallbackButton';
6270 }
6271
6272 this._buttonSelector.find($selector).remove();
6273 },
6274
6275 /**
6276 * Callback for file uploads.
6277 *
6278 * @param object event
6279 * @param File file
6280 * @param Blob blob
6281 * @return integer
6282 */
6283 _upload: function(event, file, blob) {
6284 var $uploadID = null;
6285 var $files = [ ];
6286 if (file) {
6287 $files.push(file);
6288 }
6289 else if (blob) {
6290 var $ext = '';
6291 switch (blob.type) {
6292 case 'image/png':
6293 $ext = '.png';
6294 break;
6295
6296 case 'image/jpeg':
6297 $ext = '.jpg';
6298 break;
6299
6300 case 'image/gif':
6301 $ext = '.gif';
6302 break;
6303 }
6304
6305 $files.push({
6306 name: 'pasted-from-clipboard' + $ext
6307 });
6308 }
6309 else {
6310 $files = this._fileUpload.prop('files');
6311 }
6312
6313 if ($files.length) {
6314 var $fd = new FormData();
6315 $uploadID = this._createUploadMatrix($files);
6316
6317 // no more files left, abort
6318 if (!this._uploadMatrix[$uploadID].length) {
6319 return null;
6320 }
6321
6322 for (var $i = 0, $length = $files.length; $i < $length; $i++) {
6323 if (this._uploadMatrix[$uploadID][$i]) {
6324 var $internalFileID = this._uploadMatrix[$uploadID][$i].data('internalFileID');
6325
6326 if (blob) {
6327 $fd.append('__files[' + $internalFileID + ']', blob, $files[$i].name);
6328 }
6329 else {
6330 $fd.append('__files[' + $internalFileID + ']', $files[$i]);
6331 }
6332 }
6333 }
6334
6335 $fd.append('actionName', this._options.action);
6336 $fd.append('className', this._className);
6337 var $additionalParameters = this._getParameters();
6338 for (var $name in $additionalParameters) {
6339 $fd.append('parameters[' + $name + ']', $additionalParameters[$name]);
6340 }
6341
6342 var self = this;
6343 $.ajax({
6344 type: 'POST',
6345 url: this._options.url,
6346 enctype: 'multipart/form-data',
6347 data: $fd,
6348 contentType: false,
6349 processData: false,
6350 success: function(data, textStatus, jqXHR) {
6351 self._success($uploadID, data);
6352 },
6353 error: $.proxy(this._error, this),
6354 xhr: function() {
6355 var $xhr = $.ajaxSettings.xhr();
6356 if ($xhr) {
6357 $xhr.upload.addEventListener('progress', function(event) {
6358 self._progress($uploadID, event);
6359 }, false);
6360 }
6361 return $xhr;
6362 }
6363 });
6364 }
6365
6366 return $uploadID;
6367 },
6368
6369 /**
6370 * Creates upload matrix for provided files.
6371 *
6372 * @param array<object> files
6373 * @return integer
6374 */
6375 _createUploadMatrix: function(files) {
6376 if (files.length) {
6377 var $uploadID = this._uploadMatrix.length;
6378 this._uploadMatrix[$uploadID] = [ ];
6379
6380 for (var $i = 0, $length = files.length; $i < $length; $i++) {
6381 var $file = files[$i];
6382 var $li = this._initFile($file);
6383
6384 if (!$li.hasClass('uploadFailed')) {
6385 $li.data('filename', $file.name).data('internalFileID', this._internalFileID++);
6386 this._uploadMatrix[$uploadID][$i] = $li;
6387 }
6388 }
6389
6390 return $uploadID;
6391 }
6392
6393 return null;
6394 },
6395
6396 /**
6397 * Callback for success event.
6398 *
6399 * @param integer uploadID
6400 * @param object data
6401 */
6402 _success: function(uploadID, data) { },
6403
6404 /**
6405 * Callback for error event.
6406 *
6407 * @param jQuery jqXHR
6408 * @param string textStatus
6409 * @param string errorThrown
6410 */
6411 _error: function(jqXHR, textStatus, errorThrown) { },
6412
6413 /**
6414 * Callback for progress event.
6415 *
6416 * @param integer uploadID
6417 * @param object event
6418 */
6419 _progress: function(uploadID, event) {
6420 var $percentComplete = Math.round(event.loaded * 100 / event.total);
6421
6422 for (var $i in this._uploadMatrix[uploadID]) {
6423 this._uploadMatrix[uploadID][$i].find('progress').attr('value', $percentComplete);
6424 }
6425 },
6426
6427 /**
6428 * Returns additional parameters.
6429 *
6430 * @return object
6431 */
6432 _getParameters: function() {
6433 return {};
6434 },
6435
6436 /**
6437 * Initializes list item for uploaded file.
6438 *
6439 * @return jQuery
6440 */
6441 _initFile: function(file) {
6442 return $('<li>' + file.name + ' (' + file.size + ')<progress max="100" /></li>').appendTo(this._fileListSelector);
6443 },
6444
6445 /**
6446 * Shows the fallback overlay (work in progress)
6447 */
6448 _showOverlay: function() {
6449 // create iframe
6450 if (this._iframe === null) {
6451 this._iframe = $('<iframe name="__fileUploadIFrame" />').hide().appendTo(document.body);
6452 }
6453
6454 // create overlay
6455 if (!this._overlay) {
6456 this._overlay = $('<div><form enctype="multipart/form-data" method="post" action="' + this._options.url + '" target="__fileUploadIFrame" /></div>').hide().appendTo(document.body);
6457
6458 var $form = this._overlay.find('form');
6459 $('<dl class="wide"><dd><input type="file" id="__fileUpload" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/></dd></dl>').appendTo($form);
6460 $('<div class="formSubmit"><input type="submit" value="Upload" accesskey="s" /></div></form>').appendTo($form);
6461
6462 $('<input type="hidden" name="isFallback" value="1" />').appendTo($form);
6463 $('<input type="hidden" name="actionName" value="' + this._options.action + '" />').appendTo($form);
6464 $('<input type="hidden" name="className" value="' + this._className + '" />').appendTo($form);
6465 var $additionalParameters = this._getParameters();
6466 for (var $name in $additionalParameters) {
6467 $('<input type="hidden" name="' + $name + '" value="' + $additionalParameters[$name] + '" />').appendTo($form);
6468 }
6469
6470 $form.submit($.proxy(function() {
6471 var $file = {
6472 name: this._getFilename(),
6473 size: ''
6474 };
6475
6476 var $uploadID = this._createUploadMatrix([ $file ]);
6477 var self = this;
6478 this._iframe.data('loading', true).off('load').load(function() { self._evaluateResponse($uploadID); });
6479 this._overlay.wcfDialog('close');
6480 }, this));
6481 }
6482
6483 this._overlay.wcfDialog({
6484 title: WCF.Language.get('wcf.global.button.upload')
6485 });
6486 },
6487
6488 /**
6489 * Evaluates iframe response.
6490 *
6491 * @param integer uploadID
6492 */
6493 _evaluateResponse: function(uploadID) {
6494 var $returnValues = $.parseJSON(this._iframe.contents().find('pre').html());
6495 this._success(uploadID, $returnValues);
6496 },
6497
6498 /**
6499 * Returns name of selected file.
6500 *
6501 * @return string
6502 */
6503 _getFilename: function() {
6504 return $('#__fileUpload').val().split('\\').pop();
6505 }
6506 });
6507
6508 /**
6509 * Default implementation for parallel AJAX file uploads.
6510 *
6511 * @deprecated Use WoltLab/WCF/Upload
6512 */
6513 WCF.Upload.Parallel = WCF.Upload.extend({
6514 /**
6515 * @see WCF.Upload.init()
6516 */
6517 init: function(buttonSelector, fileListSelector, className, options) {
6518 // force multiple uploads
6519 options = $.extend(true, options || { }, {
6520 multiple: true
6521 });
6522
6523 this._super(buttonSelector, fileListSelector, className, options);
6524 },
6525
6526 /**
6527 * @see WCF.Upload._upload()
6528 */
6529 _upload: function() {
6530 var $files = this._fileUpload.prop('files');
6531 for (var $i = 0, $length = $files.length; $i < $length; $i++) {
6532 var $file = $files[$i];
6533 var $formData = new FormData();
6534 var $internalFileID = this._createUploadMatrix($file);
6535
6536 if (!this._uploadMatrix[$internalFileID].length) {
6537 continue;
6538 }
6539
6540 $formData.append('__files[' + $internalFileID + ']', $file);
6541 $formData.append('actionName', this._options.action);
6542 $formData.append('className', this._className);
6543 var $additionalParameters = this._getParameters();
6544 for (var $name in $additionalParameters) {
6545 $formData.append('parameters[' + $name + ']', $additionalParameters[$name]);
6546 }
6547
6548 this._sendRequest($internalFileID, $formData);
6549 }
6550 },
6551
6552 /**
6553 * Sends an AJAX request to upload a file.
6554 *
6555 * @param integer internalFileID
6556 * @param FormData formData
6557 */
6558 _sendRequest: function(internalFileID, formData) {
6559 var self = this;
6560 $.ajax({
6561 type: 'POST',
6562 url: this._options.url,
6563 enctype: 'multipart/form-data',
6564 data: formData,
6565 contentType: false,
6566 processData: false,
6567 success: function(data, textStatus, jqXHR) {
6568 self._success(internalFileID, data);
6569 },
6570 error: $.proxy(this._error, this),
6571 xhr: function() {
6572 var $xhr = $.ajaxSettings.xhr();
6573 if ($xhr) {
6574 $xhr.upload.addEventListener('progress', function(event) {
6575 self._progress(internalFileID, event);
6576 }, false);
6577 }
6578 return $xhr;
6579 }
6580 });
6581 },
6582
6583 /**
6584 * Creates upload matrix for provided file and returns its internal file id.
6585 *
6586 * @param object file
6587 * @return integer
6588 */
6589 _createUploadMatrix: function(file) {
6590 var $li = this._initFile(file);
6591 if (!$li.hasClass('uploadFailed')) {
6592 $li.data('filename', file.name).data('internalFileID', this._internalFileID);
6593 this._uploadMatrix[this._internalFileID++] = $li;
6594
6595 return this._internalFileID - 1;
6596 }
6597
6598 return null;
6599 },
6600
6601 /**
6602 * Callback for success event.
6603 *
6604 * @param integer internalFileID
6605 * @param object data
6606 */
6607 _success: function(internalFileID, data) { },
6608
6609 /**
6610 * Callback for progress event.
6611 *
6612 * @param integer internalFileID
6613 * @param object event
6614 */
6615 _progress: function(internalFileID, event) {
6616 var $percentComplete = Math.round(event.loaded * 100 / event.total);
6617
6618 this._uploadMatrix[internalFileID].find('progress').attr('value', $percentComplete);
6619 },
6620
6621 /**
6622 * @see WCF.Upload._showOverlay()
6623 */
6624 _showOverlay: function() {
6625 // create iframe
6626 if (this._iframe === null) {
6627 this._iframe = $('<iframe name="__fileUploadIFrame" />').hide().appendTo(document.body);
6628 }
6629
6630 // create overlay
6631 if (!this._overlay) {
6632 this._overlay = $('<div><form enctype="multipart/form-data" method="post" action="' + this._options.url + '" target="__fileUploadIFrame" /></div>').hide().appendTo(document.body);
6633
6634 var $form = this._overlay.find('form');
6635 $('<dl class="wide"><dd><input type="file" id="__fileUpload" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/></dd></dl>').appendTo($form);
6636 $('<div class="formSubmit"><input type="submit" value="Upload" accesskey="s" /></div></form>').appendTo($form);
6637
6638 $('<input type="hidden" name="isFallback" value="1" />').appendTo($form);
6639 $('<input type="hidden" name="actionName" value="' + this._options.action + '" />').appendTo($form);
6640 $('<input type="hidden" name="className" value="' + this._className + '" />').appendTo($form);
6641 var $additionalParameters = this._getParameters();
6642 for (var $name in $additionalParameters) {
6643 $('<input type="hidden" name="' + $name + '" value="' + $additionalParameters[$name] + '" />').appendTo($form);
6644 }
6645
6646 $form.submit($.proxy(function() {
6647 var $file = {
6648 name: this._getFilename(),
6649 size: ''
6650 };
6651
6652 var $internalFileID = this._createUploadMatrix($file);
6653 var self = this;
6654 this._iframe.data('loading', true).off('load').load(function() { self._evaluateResponse($internalFileID); });
6655 this._overlay.wcfDialog('close');
6656 }, this));
6657 }
6658
6659 this._overlay.wcfDialog({
6660 title: WCF.Language.get('wcf.global.button.upload')
6661 });
6662 },
6663
6664 /**
6665 * Evaluates iframe response.
6666 *
6667 * @param integer internalFileID
6668 */
6669 _evaluateResponse: function(internalFileID) {
6670 var $returnValues = $.parseJSON(this._iframe.contents().find('pre').html());
6671 this._success(internalFileID, $returnValues);
6672 }
6673 });
6674
6675 /**
6676 * Namespace for sortables.
6677 */
6678 WCF.Sortable = { };
6679
6680 /**
6681 * Sortable implementation for lists.
6682 *
6683 * @param string containerID
6684 * @param string className
6685 * @param integer offset
6686 * @param object options
6687 */
6688 WCF.Sortable.List = Class.extend({
6689 /**
6690 * additional parameters for AJAX request
6691 * @var object
6692 */
6693 _additionalParameters: { },
6694
6695 /**
6696 * action class name
6697 * @var string
6698 */
6699 _className: '',
6700
6701 /**
6702 * container id
6703 * @var string
6704 */
6705 _containerID: '',
6706
6707 /**
6708 * container object
6709 * @var jQuery
6710 */
6711 _container: null,
6712
6713 /**
6714 * notification object
6715 * @var WCF.System.Notification
6716 */
6717 _notification: null,
6718
6719 /**
6720 * show order offset
6721 * @var integer
6722 */
6723 _offset: 0,
6724
6725 /**
6726 * list of options
6727 * @var object
6728 */
6729 _options: { },
6730
6731 /**
6732 * proxy object
6733 * @var WCF.Action.Proxy
6734 */
6735 _proxy: null,
6736
6737 /**
6738 * object structure
6739 * @var object
6740 */
6741 _structure: { },
6742
6743 /**
6744 * Creates a new sortable list.
6745 *
6746 * @param string containerID
6747 * @param string className
6748 * @param integer offset
6749 * @param object options
6750 * @param boolean isSimpleSorting
6751 * @param object additionalParameters
6752 */
6753 init: function(containerID, className, offset, options, isSimpleSorting, additionalParameters) {
6754 this._additionalParameters = additionalParameters || { };
6755 this._containerID = $.wcfEscapeID(containerID);
6756 this._container = $('#' + this._containerID);
6757 this._className = className;
6758 this._offset = (offset) ? offset : 0;
6759 this._proxy = new WCF.Action.Proxy({
6760 success: $.proxy(this._success, this)
6761 });
6762 this._structure = { };
6763
6764 // init sortable
6765 this._options = $.extend(true, {
6766 axis: 'y',
6767 connectWith: '#' + this._containerID + ' .sortableList',
6768 disableNesting: 'sortableNoNesting',
6769 doNotClear: true,
6770 errorClass: 'sortableInvalidTarget',
6771 forcePlaceholderSize: true,
6772 helper: 'clone',
6773 items: 'li:not(.sortableNoSorting)',
6774 opacity: .6,
6775 placeholder: 'sortablePlaceholder',
6776 tolerance: 'pointer',
6777 toleranceElement: '> span'
6778 }, options || { });
6779
6780 var sortableList = $('#' + this._containerID + ' .sortableList');
6781 if (sortableList.is('tbody') && this._options.helper === 'clone') {
6782 this._options.helper = this._tableRowHelper.bind(this);
6783 }
6784
6785 if (isSimpleSorting) {
6786 sortableList.sortable(this._options);
6787 }
6788 else {
6789 sortableList.nestedSortable(this._options);
6790 }
6791
6792 if (this._className) {
6793 var $formSubmit = this._container.find('.formSubmit');
6794 if (!$formSubmit.length) {
6795 $formSubmit = this._container.next('.formSubmit');
6796 if (!$formSubmit.length) {
6797 console.debug("[WCF.Sortable.Simple] Unable to find form submit for saving, aborting.");
6798 return;
6799 }
6800 }
6801
6802 $formSubmit.children('button[data-type="submit"]').click($.proxy(this._submit, this));
6803 }
6804 },
6805
6806 /**
6807 * Fixes the width of the cells of the dragged table row.
6808 *
6809 * @param {Event} event
6810 * @param {jQuery} ui
6811 * @return {jQuery}
6812 */
6813 _tableRowHelper: function(event, ui) {
6814 ui.children('td').each(function(index, element) {
6815 element = $(element);
6816
6817 element.width(element.width());
6818 });
6819
6820 return ui;
6821 },
6822
6823 /**
6824 * Saves object structure.
6825 */
6826 _submit: function() {
6827 // reset structure
6828 this._structure = { };
6829
6830 // build structure
6831 this._container.find('.sortableList').each($.proxy(function(index, list) {
6832 var $list = $(list);
6833 var $parentID = $list.data('objectID');
6834
6835 if ($parentID !== undefined) {
6836 $list.children(this._options.items).each($.proxy(function(index, listItem) {
6837 var $objectID = $(listItem).data('objectID');
6838
6839 if (!this._structure[$parentID]) {
6840 this._structure[$parentID] = [ ];
6841 }
6842
6843 this._structure[$parentID].push($objectID);
6844 }, this));
6845 }
6846 }, this));
6847
6848 // send request
6849 var $parameters = $.extend(true, {
6850 data: {
6851 offset: this._offset,
6852 structure: this._structure
6853 }
6854 }, this._additionalParameters);
6855
6856 this._proxy.setOption('data', {
6857 actionName: 'updatePosition',
6858 className: this._className,
6859 interfaceName: 'wcf\\data\\ISortableAction',
6860 parameters: $parameters
6861 });
6862 this._proxy.sendRequest();
6863 },
6864
6865 /**
6866 * Shows notification upon success.
6867 *
6868 * @param object data
6869 * @param string textStatus
6870 * @param jQuery jqXHR
6871 */
6872 _success: function(data, textStatus, jqXHR) {
6873 if (this._notification === null) {
6874 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.edit'));
6875 }
6876
6877 this._notification.show();
6878 }
6879 });
6880
6881 WCF.Popover = Class.extend({
6882 /**
6883 * currently active element id
6884 * @var string
6885 */
6886 _activeElementID: '',
6887
6888 _identifier: '',
6889 _popoverObj: null,
6890
6891 /**
6892 * Initializes a new WCF.Popover object.
6893 *
6894 * @param string selector
6895 */
6896 init: function(selector) {
6897 if ($.browser.mobile) return;
6898
6899 // assign default values
6900 this._activeElementID = '';
6901 this._identifier = selector;
6902
6903 require(['WoltLab/WCF/Controller/Popover'], (function(popover) {
6904 popover.init({
6905 attributeName: 'legacy',
6906 className: selector,
6907 identifier: this._identifier,
6908 legacy: true,
6909 loadCallback: this._legacyLoad.bind(this)
6910 });
6911 }).bind(this));
6912 },
6913
6914 _initContainers: function() {},
6915
6916 _legacyLoad: function(objectId, popover) {
6917 this._activeElementID = objectId;
6918 this._popoverObj = popover;
6919
6920 this._loadContent();
6921 },
6922
6923 _insertContent: function(elementId, template) {
6924 this._popoverObj.setContent(this._identifier, elementId, template);
6925 }
6926 });
6927
6928 /**
6929 * Provides an extensible item list with built-in search.
6930 *
6931 * @param string itemListSelector
6932 * @param string searchInputSelector
6933 */
6934 WCF.EditableItemList = Class.extend({
6935 /**
6936 * allows custom input not recognized by search to be added
6937 * @var boolean
6938 */
6939 _allowCustomInput: false,
6940
6941 /**
6942 * action class name
6943 * @var string
6944 */
6945 _className: '',
6946
6947 /**
6948 * internal data storage
6949 * @var mixed
6950 */
6951 _data: { },
6952
6953 /**
6954 * form container
6955 * @var jQuery
6956 */
6957 _form: null,
6958
6959 /**
6960 * item list container
6961 * @var jQuery
6962 */
6963 _itemList: null,
6964
6965 /**
6966 * current object id
6967 * @var integer
6968 */
6969 _objectID: 0,
6970
6971 /**
6972 * object type id
6973 * @var integer
6974 */
6975 _objectTypeID: 0,
6976
6977 /**
6978 * search controller
6979 * @var WCF.Search.Base
6980 */
6981 _search: null,
6982
6983 /**
6984 * search input element
6985 * @var jQuery
6986 */
6987 _searchInput: null,
6988
6989 /**
6990 * Creates a new WCF.EditableItemList object.
6991 *
6992 * @param string itemListSelector
6993 * @param string searchInputSelector
6994 */
6995 init: function(itemListSelector, searchInputSelector) {
6996 this._itemList = $(itemListSelector);
6997 this._searchInput = $(searchInputSelector);
6998 this._data = { };
6999
7000 if (!this._itemList.length || !this._searchInput.length) {
7001 console.debug("[WCF.EditableItemList] Item list and/or search input do not exist, aborting.");
7002 return;
7003 }
7004
7005 this._objectID = this._getObjectID();
7006 this._objectTypeID = this._getObjectTypeID();
7007
7008 // bind item listener
7009 this._itemList.find('.jsEditableItem').click($.proxy(this._click, this));
7010
7011 // create item list
7012 if (!this._itemList.children('ul').length) {
7013 $('<ul />').appendTo(this._itemList);
7014 }
7015 this._itemList = this._itemList.children('ul');
7016
7017 // bind form submit
7018 this._form = this._itemList.parents('form').submit($.proxy(this._submit, this));
7019
7020 if (this._allowCustomInput) {
7021 var self = this;
7022 this._searchInput.keydown($.proxy(this._keyDown, this)).keypress($.proxy(this._keyPress, this)).on('paste', function() {
7023 setTimeout(function() { self._onPaste(); }, 100);
7024 });
7025 }
7026
7027 // block form submit through [ENTER]
7028 this._searchInput.parents('.dropdown').data('preventSubmit', true);
7029 },
7030
7031 /**
7032 * Handles the key down event.
7033 *
7034 * @param object event
7035 */
7036 _keyDown: function(event) {
7037 if (event === null) {
7038 return this._keyPress(null);
7039 }
7040
7041 return true;
7042 },
7043
7044 /**
7045 * Handles the key press event.
7046 *
7047 * @param object event
7048 */
7049 _keyPress: function(event) {
7050 // 44 = [,] (charCode != keyCode)
7051 if (event === null || event.charCode === 44 || event.charCode === $.ui.keyCode.ENTER || ($.browser.mozilla && event.keyCode === $.ui.keyCode.ENTER)) {
7052 if (event !== null && event.charCode === $.ui.keyCode.ENTER && this._search) {
7053 if (this._search._itemIndex !== -1) {
7054 return false;
7055 }
7056 }
7057
7058 var $value = $.trim(this._searchInput.val());
7059
7060 // read everything left from caret position
7061 if (event && event.charCode === 44) {
7062 $value = $value.substring(0, this._searchInput.getCaret());
7063 }
7064
7065 if ($value === '') {
7066 return true;
7067 }
7068
7069 this.addItem({
7070 objectID: 0,
7071 label: $value
7072 });
7073
7074 // reset input
7075 if (event && event.charCode === 44) {
7076 this._searchInput.val($.trim(this._searchInput.val().substr(this._searchInput.getCaret())));
7077 }
7078 else {
7079 this._searchInput.val('');
7080 }
7081
7082 if (event !== null) {
7083 event.stopPropagation();
7084 }
7085
7086 return false;
7087 }
7088
7089 return true;
7090 },
7091
7092 /**
7093 * Handle paste event.
7094 */
7095 _onPaste: function() {
7096 // split content by comma
7097 var $value = $.trim(this._searchInput.val());
7098 $value = $value.split(',');
7099
7100 for (var $i = 0, $length = $value.length; $i < $length; $i++) {
7101 var $label = $.trim($value[$i]);
7102 if ($label === '') {
7103 continue;
7104 }
7105
7106 this.addItem({
7107 objectID: 0,
7108 label: $label
7109 });
7110 }
7111
7112 this._searchInput.val('');
7113 },
7114
7115 /**
7116 * Loads raw data and converts it into internal structure. Override this methods
7117 * in your derived classes.
7118 *
7119 * @param object data
7120 */
7121 load: function(data) { },
7122
7123 /**
7124 * Removes an item on click.
7125 *
7126 * @param object event
7127 * @return boolean
7128 */
7129 _click: function(event) {
7130 var $element = $(event.currentTarget);
7131 var $objectID = $element.data('objectID');
7132 var $label = $element.data('label');
7133
7134 if (this._search) {
7135 this._search.removeExcludedSearchValue($label);
7136 }
7137 this._removeItem($objectID, $label);
7138
7139 $element.remove();
7140
7141 event.stopPropagation();
7142 return false;
7143 },
7144
7145 /**
7146 * Returns current object id.
7147 *
7148 * @return integer
7149 */
7150 _getObjectID: function() {
7151 return 0;
7152 },
7153
7154 /**
7155 * Returns current object type id.
7156 *
7157 * @return integer
7158 */
7159 _getObjectTypeID: function() {
7160 return 0;
7161 },
7162
7163 /**
7164 * Adds a new item to the list.
7165 *
7166 * @param object data
7167 * @return boolean
7168 */
7169 addItem: function(data) {
7170 if (this._data[data.objectID]) {
7171 if (!(data.objectID === 0 && this._allowCustomInput)) {
7172 return true;
7173 }
7174 }
7175
7176 var $listItem = $('<li class="badge">' + WCF.String.escapeHTML(data.label) + '</li>').data('objectID', data.objectID).data('label', data.label).appendTo(this._itemList);
7177 $listItem.click($.proxy(this._click, this));
7178
7179 if (this._search) {
7180 this._search.addExcludedSearchValue(data.label);
7181 }
7182 this._addItem(data.objectID, data.label);
7183
7184 return true;
7185 },
7186
7187 /**
7188 * Clears the list of items.
7189 */
7190 clearList: function() {
7191 this._itemList.children('li').each($.proxy(function(index, element) {
7192 var $element = $(element);
7193
7194 if (this._search) {
7195 this._search.removeExcludedSearchValue($element.data('label'));
7196 }
7197
7198 $element.remove();
7199 this._removeItem($element.data('objectID'), $element.data('label'));
7200 }, this));
7201 },
7202
7203 /**
7204 * Handles form submit, override in your class.
7205 */
7206 _submit: function() {
7207 this._keyDown(null);
7208 },
7209
7210 /**
7211 * Adds an item to internal storage.
7212 *
7213 * @param integer objectID
7214 * @param string label
7215 */
7216 _addItem: function(objectID, label) {
7217 this._data[objectID] = label;
7218 },
7219
7220 /**
7221 * Removes an item from internal storage.
7222 *
7223 * @param integer objectID
7224 * @param string label
7225 */
7226 _removeItem: function(objectID, label) {
7227 delete this._data[objectID];
7228 },
7229
7230 /**
7231 * Returns the search input field.
7232 *
7233 * @return jQuery
7234 */
7235 getSearchInput: function() {
7236 return this._searchInput;
7237 }
7238 });
7239
7240 /**
7241 * Provides a language chooser.
7242 *
7243 * @param string containerID
7244 * @param string inputFieldID
7245 * @param integer languageID
7246 * @param object languages
7247 * @param object callback
7248 */
7249 WCF.Language.Chooser = Class.extend({
7250 /**
7251 * callback object
7252 * @var object
7253 */
7254 _callback: null,
7255
7256 /**
7257 * dropdown object
7258 * @var jQuery
7259 */
7260 _dropdown: null,
7261
7262 /**
7263 * input field
7264 * @var jQuery
7265 */
7266 _input: null,
7267
7268 /**
7269 * Initializes the language chooser.
7270 *
7271 * @param string containerID
7272 * @param string inputFieldID
7273 * @param integer languageID
7274 * @param object languages
7275 * @param object callback
7276 * @param boolean allowEmptyValue
7277 */
7278 init: function(containerID, inputFieldID, languageID, languages, callback, allowEmptyValue) {
7279 var $container = $('#' + containerID);
7280 if ($container.length != 1) {
7281 console.debug("[WCF.Language.Chooser] Invalid container id '" + containerID + "' given");
7282 return;
7283 }
7284
7285 // bind language id input
7286 this._input = $('#' + inputFieldID);
7287 if (!this._input.length) {
7288 this._input = $('<input type="hidden" name="' + inputFieldID + '" value="' + languageID + '" />').appendTo($container);
7289 }
7290
7291 // handle callback
7292 if (callback !== undefined) {
7293 if (!$.isFunction(callback)) {
7294 console.debug("[WCF.Language.Chooser] Given callback is invalid");
7295 return;
7296 }
7297
7298 this._callback = callback;
7299 }
7300
7301 // create language dropdown
7302 this._dropdown = $('<div class="dropdown" id="' + containerID + '-languageChooser" />').appendTo($container);
7303 $('<div class="dropdownToggle boxFlag box24" data-toggle="' + containerID + '-languageChooser"></div>').appendTo(this._dropdown);
7304 var $dropdownMenu = $('<ul class="dropdownMenu" />').appendTo(this._dropdown);
7305
7306 for (var $languageID in languages) {
7307 var $language = languages[$languageID];
7308 var $item = $('<li class="boxFlag"><a class="box24"><div><img src="' + $language.iconPath + '" alt="" class="iconFlag" /></div> <div><h3>' + $language.languageName + '</h3></div></a></li>').appendTo($dropdownMenu);
7309 $item.data('languageID', $languageID).click($.proxy(this._click, this));
7310
7311 // update dropdown label
7312 if ($languageID == languageID) {
7313 var $html = $('' + $item.html());
7314 var $innerContent = $html.children().detach();
7315 this._dropdown.children('.dropdownToggle').empty().append($innerContent);
7316 }
7317 }
7318
7319 // allow an empty selection (e.g. using as language filter)
7320 if (allowEmptyValue) {
7321 $('<li class="dropdownDivider" />').appendTo($dropdownMenu);
7322 var $item = $('<li><a>' + WCF.Language.get('wcf.global.language.noSelection') + '</a></li>').data('languageID', 0).click($.proxy(this._click, this)).appendTo($dropdownMenu);
7323
7324 if (languageID === 0) {
7325 this._dropdown.children('.dropdownToggle').empty().append($item.html());
7326 }
7327 }
7328 else if (languageID === 0) {
7329 var $dropdownToggle = $('<div><span class="icon icon24 fa-question" /></div> <div><h3>' + WCF.Language.get('wcf.global.language.noSelection') + '</h3></div>');
7330 this._dropdown.children('.dropdownToggle').empty().append($dropdownToggle);
7331 }
7332
7333 WCF.Dropdown.destroy(containerID + '-languageChooser');
7334 WCF.Dropdown.init();
7335 },
7336
7337 /**
7338 * Handles click events.
7339 *
7340 * @param object event
7341 */
7342 _click: function(event) {
7343 var $item = $(event.currentTarget);
7344 var $languageID = $item.data('languageID');
7345
7346 // update input field
7347 this._input.val($languageID);
7348
7349 // update dropdown label
7350 var $html = $('' + $item.html());
7351 var $innerContent = ($languageID === 0) ? $html : $html.children().detach();
7352 this._dropdown.children('.dropdownToggle').empty().append($innerContent);
7353
7354 // execute callback
7355 if (this._callback !== null) {
7356 this._callback($item);
7357 }
7358 }
7359 });
7360
7361 /**
7362 * Namespace for style related classes.
7363 */
7364 WCF.Style = { };
7365
7366 /**
7367 * Converts static user panel items into interactive dropdowns.
7368 *
7369 * @deprecated 2.1 - Please use WCF.User.Panel.Interactive instead
7370 *
7371 * @param string containerID
7372 */
7373 WCF.UserPanel = Class.extend({
7374 /**
7375 * target container
7376 * @var jQuery
7377 */
7378 _container: null,
7379
7380 /**
7381 * initialization state
7382 * @var boolean
7383 */
7384 _didLoad: false,
7385
7386 /**
7387 * original link element
7388 * @var jQuery
7389 */
7390 _link: null,
7391
7392 /**
7393 * language variable name for 'no items'
7394 * @var string
7395 */
7396 _noItems: '',
7397
7398 /**
7399 * reverts to original link if return values are empty
7400 * @var boolean
7401 */
7402 _revertOnEmpty: true,
7403
7404 /**
7405 * Initialites the WCF.UserPanel class.
7406 *
7407 * @param string containerID
7408 */
7409 init: function(containerID) {
7410 this._container = $('#' + containerID);
7411 this._didLoad = false;
7412 this._revertOnEmpty = true;
7413
7414 if (this._container.length != 1) {
7415 console.debug("[WCF.UserPanel] Unable to find container identfied by '" + containerID + "', aborting.");
7416 return;
7417 }
7418
7419 this._convert();
7420 },
7421
7422 /**
7423 * Converts link into an interactive dropdown menu.
7424 */
7425 _convert: function() {
7426 this._container.addClass('dropdown');
7427 this._link = this._container.children('a').remove();
7428
7429 var $button = $('<a href="' + this._link.attr('href') + '" class="dropdownToggle">' + this._link.html() + '</a>').appendTo(this._container).click($.proxy(this._click, this));
7430 var $dropdownMenu = $('<ul class="dropdownMenu" />').appendTo(this._container);
7431 $('<li class="jsDropdownPlaceholder"><span>' + WCF.Language.get('wcf.global.loading') + '</span></li>').appendTo($dropdownMenu);
7432
7433 this._addDefaultItems($dropdownMenu);
7434
7435 this._container.dblclick($.proxy(function() {
7436 window.location = this._link.attr('href');
7437 return false;
7438 }, this));
7439
7440 WCF.Dropdown.initDropdown($button, false);
7441 },
7442
7443 /**
7444 * Adds default items to dropdown menu.
7445 *
7446 * @param jQuery dropdownMenu
7447 */
7448 _addDefaultItems: function(dropdownMenu) { },
7449
7450 /**
7451 * Adds a dropdown divider.
7452 *
7453 * @param jQuery dropdownMenu
7454 */
7455 _addDivider: function(dropdownMenu) {
7456 $('<li class="dropdownDivider" />').appendTo(dropdownMenu);
7457 },
7458
7459 /**
7460 * Handles clicks on the dropdown item.
7461 *
7462 * @param object event
7463 */
7464 _click: function(event) {
7465 event.preventDefault();
7466
7467 if (this._didLoad) {
7468 return;
7469 }
7470
7471 new WCF.Action.Proxy({
7472 autoSend: true,
7473 data: this._getParameters(),
7474 success: $.proxy(this._success, this)
7475 });
7476
7477 this._didLoad = true;
7478 },
7479
7480 /**
7481 * Returns a list of parameters for AJAX request.
7482 *
7483 * @return object
7484 */
7485 _getParameters: function() {
7486 return { };
7487 },
7488
7489 /**
7490 * Handles successful AJAX requests.
7491 *
7492 * @param object data
7493 * @param string textStatus
7494 * @param jQuery jqXHR
7495 */
7496 _success: function(data, textStatus, jqXHR) {
7497 var $dropdownMenu = WCF.Dropdown.getDropdownMenu(this._container.wcfIdentify());
7498 $dropdownMenu.children('.jsDropdownPlaceholder').remove();
7499
7500 if (data.returnValues && data.returnValues.template) {
7501 $('' + data.returnValues.template).prependTo($dropdownMenu);
7502
7503 // update badge
7504 this._updateBadge(data.returnValues.totalCount);
7505
7506 this._after($dropdownMenu);
7507 }
7508 else {
7509 $('<li><span>' + WCF.Language.get(this._noItems) + '</span></li>').prependTo($dropdownMenu);
7510
7511 // remove badge
7512 this._updateBadge(0);
7513 }
7514 },
7515
7516 /**
7517 * Updates badge count.
7518 *
7519 * @param integer count
7520 */
7521 _updateBadge: function(count) {
7522 count = parseInt(count) || 0;
7523
7524 if (count) {
7525 var $badge = this._container.find('.badge');
7526 if (!$badge.length) {
7527 $badge = $('<span class="badge badgeUpdate" />').appendTo(this._container.children('.dropdownToggle'));
7528 $badge.before(' ');
7529 }
7530 $badge.html(count);
7531 }
7532 else {
7533 this._container.find('.badge').remove();
7534 }
7535 },
7536
7537 /**
7538 * Execute actions after the dropdown menu has been populated.
7539 *
7540 * @param object dropdownMenu
7541 */
7542 _after: function(dropdownMenu) { }
7543 });
7544
7545 jQuery.fn.extend({
7546 // shim for 'ui.wcfDialog'
7547 wcfDialog: function(method) {
7548 var args = arguments;
7549
7550 require(['Dom/Util', 'Ui/Dialog'], (function(DomUtil, UiDialog) {
7551 var id = DomUtil.identify(this[0]);
7552
7553 if (method === 'close') {
7554 UiDialog.close(id);
7555 }
7556 else if (method === 'render') {
7557 UiDialog.rebuild(id);
7558 }
7559 else if (method === 'option') {
7560 if (args.length === 3 && args[1] === 'title' && typeof args[2] === 'string') {
7561 UiDialog.setTitle(id, args[2]);
7562 }
7563 }
7564 else {
7565 if (this[0].parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
7566 // if element is not already part of the DOM, UiDialog.open() will fail
7567 document.body.appendChild(this[0]);
7568 }
7569
7570 UiDialog.openStatic(id, null, (args.length === 1 && typeof args[0] === 'object') ? args[0] : {});
7571 }
7572 }).bind(this));
7573
7574 return this;
7575 }
7576 });
7577
7578 /**
7579 * Provides a slideshow for lists.
7580 */
7581 $.widget('ui.wcfSlideshow', {
7582 /**
7583 * button list object
7584 * @var jQuery
7585 */
7586 _buttonList: null,
7587
7588 /**
7589 * number of items
7590 * @var integer
7591 */
7592 _count: 0,
7593
7594 /**
7595 * item index
7596 * @var integer
7597 */
7598 _index: 0,
7599
7600 /**
7601 * item list object
7602 * @var jQuery
7603 */
7604 _itemList: null,
7605
7606 /**
7607 * list of items
7608 * @var jQuery
7609 */
7610 _items: null,
7611
7612 /**
7613 * timer object
7614 * @var WCF.PeriodicalExecuter
7615 */
7616 _timer: null,
7617
7618 /**
7619 * list item width
7620 * @var integer
7621 */
7622 _width: 0,
7623
7624 /**
7625 * list of options
7626 * @var object
7627 */
7628 options: {
7629 /* enables automatic cycling of items */
7630 cycle: true,
7631 /* cycle interval in seconds */
7632 cycleInterval: 5,
7633 /* gap between items in pixels */
7634 itemGap: 50
7635 },
7636
7637 /**
7638 * Creates a new instance of ui.wcfSlideshow.
7639 */
7640 _create: function() {
7641 this._itemList = this.element.children('ul');
7642 this._items = this._itemList.children('li');
7643 this._count = this._items.length;
7644 this._index = 0;
7645
7646 if (this._count > 1) {
7647 this._initSlideshow();
7648 }
7649 },
7650
7651 /**
7652 * Initializes the slideshow.
7653 */
7654 _initSlideshow: function() {
7655 // calculate item dimensions
7656 var $itemHeight = $(this._items.get(0)).outerHeight();
7657 this._items.addClass('slideshowItem');
7658 this._width = this.element.css('height', $itemHeight).innerWidth();
7659 this._itemList.addClass('slideshowItemList').css('left', 0);
7660
7661 this._items.each($.proxy(function(index, item) {
7662 $(item).show().css({
7663 height: $itemHeight,
7664 left: ((this._width + this.options.itemGap) * index),
7665 width: this._width
7666 });
7667 }, this));
7668
7669 this.element.css({
7670 height: $itemHeight,
7671 width: this._width
7672 }).hover($.proxy(this._hoverIn, this), $.proxy(this._hoverOut, this));
7673
7674 // create toggle buttons
7675 this._buttonList = $('<ul class="slideshowButtonList" />').appendTo(this.element);
7676 for (var $i = 0; $i < this._count; $i++) {
7677 var $link = $('<li><a><span class="icon icon16 fa-circle" /></a></li>').data('index', $i).click($.proxy(this._click, this)).appendTo(this._buttonList);
7678 if ($i == 0) {
7679 $link.find('.icon').addClass('active');
7680 }
7681 }
7682
7683 this._resetTimer();
7684
7685 $(window).resize($.proxy(this._resize, this));
7686 },
7687
7688 /**
7689 * Rebuilds slideshow height in case the initial height contained resources affecting the
7690 * element height, but loading has not completed on slideshow init.
7691 */
7692 rebuildHeight: function() {
7693 var $firstItem = $(this._items.get(0)).css('height', 'auto');
7694 var $itemHeight = $firstItem.outerHeight();
7695
7696 this._items.css('height', $itemHeight + 'px');
7697 this.element.css('height', $itemHeight + 'px');
7698 },
7699
7700 /**
7701 * Handles browser resizing
7702 */
7703 _resize: function() {
7704 this._width = this.element.css('width', 'auto').innerWidth();
7705 this._items.each($.proxy(function(index, item) {
7706 $(item).css({
7707 left: ((this._width + this.options.itemGap) * index),
7708 width: this._width
7709 });
7710 }, this));
7711
7712 this._index--;
7713 this.moveTo(null);
7714 },
7715
7716 /**
7717 * Disables cycling while hovering.
7718 */
7719 _hoverIn: function() {
7720 if (this._timer !== null) {
7721 this._timer.stop();
7722 }
7723 },
7724
7725 /**
7726 * Enables cycling after mouse out.
7727 */
7728 _hoverOut: function() {
7729 this._resetTimer();
7730 },
7731
7732 /**
7733 * Resets cycle timer.
7734 */
7735 _resetTimer: function() {
7736 if (!this.options.cycle) {
7737 return;
7738 }
7739
7740 if (this._timer !== null) {
7741 this._timer.stop();
7742 }
7743
7744 var self = this;
7745 this._timer = new WCF.PeriodicalExecuter(function() {
7746 self.moveTo(null);
7747 }, this.options.cycleInterval * 1000);
7748 },
7749
7750 /**
7751 * Handles clicks on the select buttons.
7752 *
7753 * @param object event
7754 */
7755 _click: function(event) {
7756 this.moveTo($(event.currentTarget).data('index'));
7757
7758 this._resetTimer();
7759 },
7760
7761 /**
7762 * Moves to a specified item index, NULL will move to the next item in list.
7763 *
7764 * @param integer index
7765 */
7766 moveTo: function(index) {
7767 this._index = (index === null) ? this._index + 1 : index;
7768 if (this._index == this._count) {
7769 this._index = 0;
7770 }
7771
7772 $(this._buttonList.find('.icon').removeClass('active').get(this._index)).addClass('active');
7773 this._itemList.css('left', this._index * (this._width + this.options.itemGap) * -1);
7774
7775 this._trigger('moveTo', null, { index: this._index });
7776 },
7777
7778 /**
7779 * Returns item by index or null if index is invalid.
7780 *
7781 * @return jQuery
7782 */
7783 getItem: function(index) {
7784 if (this._items[index]) {
7785 return this._items[index];
7786 }
7787
7788 return null;
7789 }
7790 });
7791
7792 jQuery.fn.extend({
7793 datepicker: function(method) {
7794 var element = this[0], parameters = Array.prototype.slice.call(arguments, 1);
7795
7796 switch (method) {
7797 case 'destroy':
7798 window.__wcf_bc_datePicker.destroy(element);
7799 break;
7800
7801 case 'getDate':
7802 return window.__wcf_bc_datePicker.getDate(element);
7803 break;
7804
7805 case 'option':
7806 if (parameters[0] === 'onClose') {
7807 return function() {};
7808 }
7809
7810 console.warn("datepicker('option') supports only 'onClose'.");
7811 break;
7812
7813 case 'setDate':
7814 window.__wcf_bc_datePicker.setDate(element, parameters[0]);
7815 break;
7816
7817 case 'setOption':
7818 if (parameters[0] === 'onClose') {
7819 window.__wcf_bc_datePicker.setCloseCallback(element, parameters[1]);
7820 }
7821 else {
7822 console.warn("datepicker('setOption') supports only 'onClose'.");
7823 }
7824 break;
7825
7826 default:
7827 console.debug("Unsupported method '" + method + "' for datepicker()");
7828 break;
7829 }
7830 }
7831 });
7832
7833 jQuery.fn.extend({
7834 wcfTabs: function(method) {
7835 var element = this[0], parameters = Array.prototype.slice.call(arguments, 1);
7836
7837 require(['Dom/Util', 'WoltLab/WCF/Ui/TabMenu'], function(DomUtil, TabMenu) {
7838 var container = TabMenu.getTabMenu(DomUtil.identify(element));
7839 if (container !== null) {
7840 container[method].apply(container, parameters);
7841 }
7842 });
7843 }
7844 });
7845
7846 /**
7847 * jQuery widget implementation of the wcf pagination.
7848 *
7849 * @deprecated 2.2 - use `WoltLab/WCF/Ui/Pagination` instead
7850 */
7851 $.widget('ui.wcfPages', {
7852 _api: null,
7853
7854 SHOW_LINKS: 11,
7855 SHOW_SUB_LINKS: 20,
7856
7857 options: {
7858 // vars
7859 activePage: 1,
7860 maxPage: 1
7861 },
7862
7863 /**
7864 * Creates the pages widget.
7865 */
7866 _create: function() {
7867 require(['WoltLab/WCF/Ui/Pagination'], (function(UiPagination) {
7868 this._api = new UiPagination(this.element, {
7869 activePage: this.options.activePage,
7870 maxPage: this.options.maxPage,
7871
7872 callbackShouldSwitch: (function(pageNo) {
7873 var result = this._trigger('shouldSwitch', undefined, {
7874 nextPage: pageNo
7875 });
7876
7877 return (result !== false);
7878 }).bind(this),
7879 callbackSwitch: (function(pageNo) {
7880 this._trigger('switched', undefined, {
7881 activePage: pageNo
7882 });
7883 }).bind(this)
7884 });
7885 }).bind(this));
7886 },
7887
7888 /**
7889 * Destroys the pages widget.
7890 */
7891 destroy: function() {
7892 $.Widget.prototype.destroy.apply(this, arguments);
7893
7894 this._api = null;
7895 this.element[0].innerHTML = '';
7896 },
7897
7898 /**
7899 * Sets the given option to the given value.
7900 * See the jQuery UI widget documentation for more.
7901 */
7902 _setOption: function(key, value) {
7903 if (key == 'activePage') {
7904 if (value != this.options[key] && value > 0 && value <= this.options.maxPage) {
7905 // you can prevent the page switching by returning false or by event.preventDefault()
7906 // in a shouldSwitch-callback. e.g. if an AJAX request is already running.
7907 var $result = this._trigger('shouldSwitch', undefined, {
7908 nextPage: value
7909 });
7910
7911 if ($result || $result !== undefined) {
7912 this._api.switchPage(value);
7913
7914 }
7915 else {
7916 this._trigger('notSwitched', undefined, {
7917 activePage: value
7918 });
7919 }
7920 }
7921 }
7922
7923 return this;
7924 }
7925 });
7926
7927 /**
7928 * Namespace for category related classes.
7929 */
7930 WCF.Category = { };
7931
7932 /**
7933 * Handles selection of categories.
7934 */
7935 WCF.Category.NestedList = Class.extend({
7936 /**
7937 * list of categories
7938 * @var object
7939 */
7940 _categories: { },
7941
7942 /**
7943 * Initializes the WCF.Category.NestedList object.
7944 */
7945 init: function() {
7946 var self = this;
7947 $('.jsCategory').each(function(index, category) {
7948 var $category = $(category).data('parentCategoryID', null).change($.proxy(self._updateSelection, self));
7949 self._categories[$category.val()] = $category;
7950
7951 // find child categories
7952 var $childCategoryIDs = [ ];
7953 $category.parents('li').find('.jsChildCategory').each(function(innerIndex, childCategory) {
7954 var $childCategory = $(childCategory).data('parentCategoryID', $category.val()).change($.proxy(self._updateSelection, self));
7955 self._categories[$childCategory.val()] = $childCategory;
7956 $childCategoryIDs.push($childCategory.val());
7957
7958 if ($childCategory.is(':checked')) {
7959 $category.prop('checked', 'checked');
7960 }
7961 });
7962
7963 $category.data('childCategoryIDs', $childCategoryIDs);
7964 });
7965 },
7966
7967 /**
7968 * Updates selection of categories.
7969 *
7970 * @param object event
7971 */
7972 _updateSelection: function(event) {
7973 var $category = $(event.currentTarget);
7974 var $parentCategoryID = $category.data('parentCategoryID');
7975
7976 if ($category.is(':checked')) {
7977 // child category
7978 if ($parentCategoryID !== null) {
7979 // mark parent category as checked
7980 this._categories[$parentCategoryID].prop('checked', 'checked');
7981 }
7982 }
7983 else {
7984 // top-level category
7985 if ($parentCategoryID === null) {
7986 // unmark all child categories
7987 var $childCategoryIDs = $category.data('childCategoryIDs');
7988 for (var $i = 0, $length = $childCategoryIDs.length; $i < $length; $i++) {
7989 this._categories[$childCategoryIDs[$i]].prop('checked', false);
7990 }
7991 }
7992 }
7993 }
7994 });
7995
7996 /**
7997 * Handles selection of categories.
7998 */
7999 WCF.Category.FlexibleCategoryList = Class.extend({
8000 /**
8001 * category list container
8002 * @var jQuery
8003 */
8004 _list: null,
8005
8006 /**
8007 * list of children per category id
8008 * @var object<integer>
8009 */
8010 _categories: { },
8011
8012 init: function(elementID) {
8013 this._list = $('#' + elementID);
8014
8015 this._buildStructure();
8016
8017 this._list.find('input:checked').each(function() {
8018 $(this).trigger('change');
8019 });
8020
8021 if (this._list.children('li').length < 2) {
8022 this._list.addClass('flexibleCategoryListDisabled');
8023 return;
8024 }
8025 },
8026
8027 _buildStructure: function() {
8028 var self = this;
8029 this._list.find('.jsCategory').each(function(i, category) {
8030 var $category = $(category).change(self._updateSelection.bind(self));
8031 var $categoryID = parseInt($category.val());
8032 var $childCategories = [ ];
8033
8034 $category.parents('li:eq(0)').find('.jsChildCategory').each(function(j, childCategory) {
8035 var $childCategory = $(childCategory);
8036 $childCategory.data('parentCategory', $category).change(self._updateSelection.bind(self));
8037
8038 var $childCategoryID = parseInt($childCategory.val());
8039 $childCategories.push($childCategory);
8040
8041 var $subChildCategories = [ ];
8042
8043 $childCategory.parents('li:eq(0)').find('.jsSubChildCategory').each(function(k, subChildCategory) {
8044 var $subChildCategory = $(subChildCategory);
8045 $subChildCategory.data('parentCategory', $childCategory).change(self._updateSelection.bind(self));
8046 $subChildCategories.push($subChildCategory);
8047 });
8048
8049 self._categories[$childCategoryID] = $subChildCategories;
8050 });
8051
8052 self._categories[$categoryID] = $childCategories;
8053 });
8054 },
8055
8056 _updateSelection: function(event) {
8057 var $category = $(event.currentTarget);
8058 var $categoryID = parseInt($category.val());
8059 var $parentCategory = $category.data('parentCategory');
8060
8061 if ($category.is(':checked')) {
8062 if ($parentCategory) {
8063 $parentCategory.prop('checked', 'checked');
8064
8065 $parentCategory = $parentCategory.data('parentCategory');
8066 if ($parentCategory) {
8067 $parentCategory.prop('checked', 'checked');
8068 }
8069 }
8070 }
8071 else {
8072 // uncheck child categories
8073 if (this._categories[$categoryID]) {
8074 for (var $i = 0, $length = this._categories[$categoryID].length; $i < $length; $i++) {
8075 var $childCategory = this._categories[$categoryID][$i];
8076 $childCategory.prop('checked', false);
8077
8078 var $childCategoryID = parseInt($childCategory.val());
8079 if (this._categories[$childCategoryID]) {
8080 for (var $j = 0, $innerLength = this._categories[$childCategoryID].length; $j < $innerLength; $j++) {
8081 this._categories[$childCategoryID][$j].prop('checked', false);
8082 }
8083 }
8084 }
8085 }
8086
8087 // uncheck direct parent if it has no more checked children
8088 if ($parentCategory) {
8089 var $parentCategoryID = parseInt($parentCategory.val());
8090 for (var $i = 0, $length = this._categories[$parentCategoryID].length; $i < $length; $i++) {
8091 if (this._categories[$parentCategoryID][$i].prop('checked')) {
8092 // at least one child is checked, break
8093 return;
8094 }
8095 }
8096
8097 $parentCategory = $parentCategory.data('parentCategory');
8098 if ($parentCategory) {
8099 $parentCategoryID = parseInt($parentCategory.val());
8100 for (var $i = 0, $length = this._categories[$parentCategoryID].length; $i < $length; $i++) {
8101 if (this._categories[$parentCategoryID][$i].prop('checked')) {
8102 // at least one child is checked, break
8103 return;
8104 }
8105 }
8106 }
8107 }
8108 }
8109 }
8110 });
8111
8112 /**
8113 * Initializes WCF.Condition namespace.
8114 */
8115 WCF.Condition = { };
8116
8117 /**
8118 * Handles displaying the a form element whose visibility depends on the selected
8119 * page controllers.
8120 */
8121 WCF.Condition.PageControllerDependence = Class.extend({
8122 /**
8123 * select list with the available page controllers
8124 * @var jQuery
8125 */
8126 _pageControllerSelection: null,
8127
8128 /**
8129 * ids of page object types that support the form element
8130 * @var array<integer>
8131 */
8132 _supportedPageObjectTypeIDs: [],
8133
8134 /**
8135 * Initializes a new WCF.Condition.PageControllerDependence object.
8136 *
8137 * @param string inputIdentifier
8138 * @param array<integer> supportedPageObjectTypeIDs
8139 */
8140 init: function(inputIdentifier, supportedPageObjectTypeIDs) {
8141 this._supportedPageObjectTypeIDs = supportedPageObjectTypeIDs;
8142
8143 this._pageControllerSelection = $('#pageControllers').change($.proxy(this._checkVisibility, this));
8144 this._pageControllerContainer = this._pageControllerSelection.parents('dl:eq(0)');
8145 this._fieldset = this._pageControllerContainer.parent('fieldset');
8146 this._nextFieldset = this._fieldset.next('fieldset');
8147 if (this._nextFieldset) {
8148 this._nextFieldset.data('margin-top', this._nextFieldset.css('margin-top'));
8149 }
8150
8151 this._input = $('#' + inputIdentifier);
8152 this._inputContainer = this._input.parents('dl:eq(0)');
8153
8154 this._checkVisibility();
8155 },
8156
8157 /**
8158 * Checks the visibility based on the selected page controllers.
8159 */
8160 _checkVisibility: function() {
8161 var $selectedPageIDs = this._pageControllerSelection.val() || [ ];
8162
8163 var $display = true;
8164 if ($selectedPageIDs.length) {
8165 for (var $i = 0, $length = $selectedPageIDs.length; $i < $length; $i++) {
8166 if (this._supportedPageObjectTypeIDs.indexOf(parseInt($selectedPageIDs[$i])) == -1) {
8167 $display = false;
8168 break;
8169 }
8170 }
8171 }
8172 else {
8173 $display = false;
8174 }
8175
8176 if ($display) {
8177 this._inputContainer.show();
8178 this._input.enable();
8179 this._fieldset.show();
8180
8181 if (this._nextFieldset) {
8182 this._nextFieldset.css('margin-top', this._nextFieldset.data('margin-top'));
8183 }
8184 }
8185 else {
8186 this._inputContainer.hide();
8187 this._input.disable();
8188
8189 if (!this._fieldset.children('dl:visible').length) {
8190 this._fieldset.hide();
8191
8192 if (this._nextFieldset) {
8193 this._nextFieldset.css('margin-top', 0);
8194 }
8195 }
8196 }
8197 }
8198 });
8199
8200 /**
8201 * Initialize WCF.Notice namespace.
8202 */
8203 WCF.Notice = { };
8204
8205 /**
8206 * Encapsulate eval() within an own function to prevent problems
8207 * with optimizing and minifiny JS.
8208 *
8209 * @param mixed expression
8210 * @returns mixed
8211 */
8212 function wcfEval(expression) {
8213 return eval(expression);
8214 }