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