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