2 * Class and function collection for WCF
4 * @author Markus Bartz, Tim Düsterhus, Alexander Ebert, Matthias Schmidt
5 * @copyright 2001-2011 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
10 // store original implementation
11 var $jQueryData
= jQuery
.fn
.data
;
14 * Override jQuery.fn.data() to support custom 'ID' suffix which will
15 * be translated to '-id' at runtime.
17 * @see jQuery.fn.data()
19 jQuery
.fn
.data = function(key
, value
) {
20 if (key
&& key
.match(/ID$/)) {
21 arguments
[0] = key
.replace(/ID$/, '-id');
24 // call jQuery's own data method
25 var $data
= $jQueryData
.apply(this, arguments
);
27 // handle .data() call without arguments
28 if (key
=== undefined) {
29 for (var $key
in $data
) {
30 if ($key
.match(/Id$/)) {
31 $data
[$key
.replace(/Id$/, 'ID')] = $data
[$key
];
42 * Simple JavaScript Inheritance
43 * By John Resig http://ejohn.org/
46 // Inspired by base2 and Prototype
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
;};})();
50 * Provides a hashCode() method for strings, similar to Java's String.hashCode().
52 * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
54 String
.prototype.hashCode = function() {
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
70 * Initialize WCF namespace
75 * Extends jQuery with additional methods.
79 * Removes the given value from the given array and returns the array.
82 * @param mixed element
85 removeArrayValue: function(array
, value
) {
86 return $.grep(array
, function(element
, index
) {
87 return value
!== element
;
92 * Escapes an ID to work with jQuery selectors.
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
98 wcfEscapeID: function(id
) {
99 return id
.replace(/(:|\.)/g, '\\$1');
103 * Returns true if given ID exists within DOM.
108 wcfIsset: function(id
) {
109 return !!$('#' + $.wcfEscapeID(id
)).length
;
113 * Returns the length of an object.
115 * @param object targetObject
118 getLength: function(targetObject
) {
121 for (var $key
in targetObject
) {
122 if (targetObject
.hasOwnProperty($key
)) {
132 * Extends jQuery's chainable methods.
136 * Returns tag name of current jQuery element.
140 getTagName: function() {
141 return this.get(0).tagName
.toLowerCase();
145 * Returns the dimensions for current element.
147 * @see http://api.jquery.com/hidden-selector/
151 getDimensions: function(type
) {
152 var dimensions
= css
= {};
153 var wasHidden
= false;
155 // show element to retrieve dimensions and restore them later
156 if (this.is(':hidden')) {
158 display
: this.css('display'),
159 visibility
: this.css('visibility')
173 height
: this.innerHeight(),
174 width
: this.innerWidth()
180 height
: this.outerHeight(),
181 width
: this.outerWidth()
187 height
: this.height(),
193 // restore previous settings
202 * Returns the offsets for current element, defaults to position
203 * relative to document.
205 * @see http://api.jquery.com/hidden-selector/
209 getOffsets: function(type
) {
210 var offsets
= css
= {};
211 var wasHidden
= false;
213 // show element to retrieve dimensions and restore them later
214 if (this.is(':hidden')) {
216 display
: this.css('display'),
217 visibility
: this.css('visibility')
230 offsets
= this.offset();
235 offsets
= this.position();
239 // restore previous settings
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)
252 * @param boolean rebase
255 makePositioned: function(position
, rebase
) {
256 if (position
!= 'absolute' && position
!= 'fixed') {
257 position
= 'absolute';
260 var $currentPosition
= this.getOffsets('position');
263 left
: $currentPosition
.left
,
265 top
: $currentPosition
.top
269 this.remove().appentTo('body');
276 * Disables a form element.
280 disable: function() {
281 return this.attr('disabled', 'disabled');
285 * Enables a form element.
290 return this.removeAttr('disabled');
294 * Returns the element's id. If none is set, a random unique
295 * ID will be assigned.
299 wcfIdentify: function() {
300 if (!this.attr('id')) {
301 this.attr('id', WCF
.getRandomID());
304 return this.attr('id');
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.
314 getCaret: function() {
315 if (this.getTagName() == 'input') {
316 if (this.attr('type') != 'text' && this.attr('type') != 'password') {
320 else if (this.getTagName() != 'textarea') {
325 var $element
= this.get(0);
326 if (document
.selection
) { // IE 8
327 // set focus to enable caret on this element
330 var $selection
= document
.selection
.createRange();
331 $selection
.moveStart('character', -this.val().length
);
332 $position
= $selection
.text
.length
;
334 else if ($element
.selectionStart
|| $element
.selectionStart
== '0') { // Opera, Chrome, Firefox, Safari, IE 9+
335 $position
= parseInt($element
.selectionStart
);
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.
346 * @param integer position
349 setCaret: function (position
) {
350 if (this.getTagName() == 'input') {
351 if (this.attr('type') != 'text' && this.attr('type') != 'password') {
355 else if (this.getTagName() != 'textarea') {
359 var $element
= this.get(0);
361 // set focus to enable caret on this element
363 if (document
.selection
) { // IE 8
364 var $selection
= document
.selection
.createRange();
365 $selection
.moveStart('character', position
);
366 $selection
.moveEnd('character', 0);
369 else if ($element
.selectionStart
|| $element
.selectionStart
== '0') { // Opera, Chrome, Firefox, Safari, IE 9+
370 $element
.selectionStart
= position
;
371 $element
.selectionEnd
= position
;
378 * Shows an element by sliding and fading it into viewport.
380 * @param string direction
381 * @param object callback
382 * @param integer duration
385 wcfDropIn: function(direction
, callback
, duration
) {
386 if (!direction
) direction
= 'up';
387 if (!duration
|| !parseInt(duration
)) duration
= 200;
389 return this.show(WCF
.getEffect(this.getTagName(), 'drop'), { direction
: direction
}, duration
, callback
);
393 * Hides an element by sliding and fading it out the viewport.
395 * @param string direction
396 * @param object callback
397 * @param integer duration
400 wcfDropOut: function(direction
, callback
, duration
) {
401 if (!direction
) direction
= 'down';
402 if (!duration
|| !parseInt(duration
)) duration
= 200;
404 return this.hide(WCF
.getEffect(this.getTagName(), 'drop'), { direction
: direction
}, duration
, callback
);
408 * Shows an element by blinding it up.
410 * @param string direction
411 * @param object callback
412 * @param integer duration
415 wcfBlindIn: function(direction
, callback
, duration
) {
416 if (!direction
) direction
= 'vertical';
417 if (!duration
|| !parseInt(duration
)) duration
= 200;
419 return this.show(WCF
.getEffect(this.getTagName(), 'blind'), { direction
: direction
}, duration
, callback
);
423 * Hides an element by blinding it down.
425 * @param string direction
426 * @param object callback
427 * @param integer duration
430 wcfBlindOut: function(direction
, callback
, duration
) {
431 if (!direction
) direction
= 'vertical';
432 if (!duration
|| !parseInt(duration
)) duration
= 200;
434 return this.hide(WCF
.getEffect(this.getTagName(), 'blind'), { direction
: direction
}, duration
, callback
);
438 * Highlights an element.
440 * @param object options
441 * @param object callback
444 wcfHighlight: function(options
, callback
) {
445 return this.effect('highlight', options
, 600, callback
);
449 * Shows an element by fading it in.
451 * @param object callback
452 * @param integer duration
455 wcfFadeIn: function(callback
, duration
) {
456 if (!duration
|| !parseInt(duration
)) duration
= 200;
458 return this.show(WCF
.getEffect(this.getTagName(), 'fade'), { }, duration
, callback
);
462 * Hides an element by fading it out.
464 * @param object callback
465 * @param integer duration
468 wcfFadeOut: function(callback
, duration
) {
469 if (!duration
|| !parseInt(duration
)) duration
= 200;
471 return this.hide(WCF
.getEffect(this.getTagName(), 'fade'), { }, duration
, callback
);
476 * WoltLab Community Framework core methods
480 * count of active dialogs
486 * Counter for dynamic element id's
493 * Shows a modal dialog with a built-in AJAX-loader.
495 * @param string dialogID
496 * @param boolean resetDialog
499 showAJAXDialog: function(dialogID
, resetDialog
) {
501 dialogID
= this.getRandomID();
504 if (!$.wcfIsset(dialogID
)) {
505 $('<div id="' + dialogID
+ '"></div>').appendTo(document
.body
);
508 var dialog
= $('#' + $.wcfEscapeID(dialogID
));
514 var dialogOptions
= arguments
[2] || {};
515 dialogOptions
.ajax
= true;
517 dialog
.wcfDialog(dialogOptions
);
523 * Shows a modal dialog.
525 * @param string dialogID
527 showDialog: function(dialogID
) {
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;
532 var $dialog
= $('#' + $.wcfEscapeID(dialogID
));
534 var dialogOptions
= arguments
[1] || {};
535 $dialog
.wcfDialog(dialogOptions
);
539 * Returns a dynamically created id.
541 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/dom/dom.js#L1789
544 getRandomID: function() {
548 $elementID
= 'wcf' + this._idCounter
++;
550 while ($.wcfIsset($elementID
));
556 * Wrapper for $.inArray which returns boolean value instead of
557 * index value, similar to PHP's in_array().
559 * @param mixed needle
560 * @param array haystack
563 inArray: function(needle
, haystack
) {
564 return ($.inArray(needle
, haystack
) != -1);
568 * Adjusts effect for partially supported elements.
570 * @param object object
571 * @param string effect
574 getEffect: function(tagName
, effect
) {
575 // most effects are not properly supported on table rows, use highlight instead
576 if (tagName
== 'tr') {
595 * initialization state
601 * list of registered dropdowns
607 * Initializes dropdowns.
610 var $userPanelHeight
= $('#topMenu').outerHeight();
612 $('.dropdownToggle').each(function(index
, button
) {
613 var $button
= $(button
);
614 if ($button
.data('target')) {
618 var $dropdown
= $button
.parents('.dropdown');
619 if (!$dropdown
.length
) {
620 // broken dropdown, ignore
624 var $containerID
= $dropdown
.wcfIdentify();
625 if (!self
._dropdowns
[$containerID
]) {
626 $button
.click($.proxy(self
._toggle
, self
));
627 self
._dropdowns
[$containerID
] = $dropdown
;
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
;
636 // calculate top offset for menu
637 $button
.next('.dropdownMenu').css({
642 $button
.data('target', $containerID
);
645 if (!this._didInit
) {
646 this._didInit
= true;
648 WCF
.CloseOverlayHandler
.addCallback('WCF.Dropdown', $.proxy(this._closeAll
, this));
649 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Dropdown', $.proxy(this.init
, this));
654 * Registers a callback notified upon dropdown state change.
656 * @param string identifier
657 * @var object callback
659 registerCallback: function(identifier
, callback
) {
660 if (!$.isFunction(callback
)) {
661 console
.debug("[WCF.Dropdown] Callback for '" + identifier
+ "' is invalid");
665 if (!this._callbacks
[identifier
]) {
666 this._callbacks
[identifier
] = [ ];
669 this._callbacks
[identifier
].push(callback
);
673 * Toggles a dropdown.
675 * @param object event
677 _toggle: function(event
) {
678 var $targetID
= $(event
.currentTarget
).data('target');
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');
687 else if ($containerID
=== $targetID
) {
689 var $dropdownMenu
= $dropdown
.find('.dropdownMenu');
690 if ($dropdownMenu
.css('top') === '7px') {
692 top
: $dropdown
.outerHeight() + 7
696 $dropdown
.addClass('dropdownOpen');
697 this._notifyCallbacks($dropdown
, 'open');
699 this.setAlignment($dropdown
);
703 event
.stopPropagation();
708 * Sets alignment for dropdown.
710 * @param jQuery dropdown
711 * @param jQuery dropdownMenu
713 setAlignment: function(dropdown
, dropdownMenu
) {
714 var $dropdownMenu
= (dropdown
) ? dropdown
.find('.dropdownMenu:eq(0)') : dropdownMenu
;
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();
721 if (($offsets
.left
+ $dimensions
.width
) > $windowWidth
) {
725 }).addClass('dropdownArrowRight');
727 else if ($dropdownMenu
.css('right') != '0px') {
731 }).removeClass('dropdownArrowRight');
736 * Closes all dropdowns.
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');
744 this._notifyCallbacks($dropdown
, 'close');
750 * Closes a dropdown without notifying callbacks.
752 * @param string containerID
754 close: function(containerID
) {
755 if (!this._dropdowns
[containerID
]) {
759 this._dropdowns
[containerID
].removeClass('open');
763 * Notifies callbacks.
765 * @param jQuery dropdown
766 * @param string action
768 _notifyCallbacks: function(dropdown
, action
) {
769 var $containerID
= dropdown
.wcfIdentify();
770 if (!this._callbacks
[$containerID
]) {
774 for (var $i
= 0, $length
= this._callbacks
[$containerID
].length
; $i
< $length
; $i
++) {
775 this._callbacks
[$containerID
][$i
](dropdown
, action
);
785 * action proxy object
786 * @var WCF.Action.Proxy
797 * list of clipboard containers
803 * container meta data
809 * user has marked items
812 _hasMarkedItems
: false,
815 * list of ids of marked objects
818 _markedObjectIDs
: [],
828 * @var WCF.Action.Proxy
833 * list of elements already tracked for clipboard actions
836 _trackedElements
: { },
839 * Initializes the clipboard API.
841 init: function(page
, hasMarkedItems
, actionObjects
) {
843 this._actionObjects
= actionObjects
;
844 if (!actionObjects
) {
845 this._actionObjects
= {};
847 if (hasMarkedItems
) {
848 this._hasMarkedItems
= true;
851 this._actionProxy
= new WCF
.Action
.Proxy({
852 success
: $.proxy(this._actionSuccess
, this),
853 url
: 'index.php/ClipboardProxy/?t=' + SECURITY_TOKEN
+ SID_ARG_2ND
856 this._proxy
= new WCF
.Action
.Proxy({
857 success
: $.proxy(this._success
, this),
858 url
: 'index.php/Clipboard/?t=' + SECURITY_TOKEN
+ SID_ARG_2ND
861 // init containers first
862 this._containers
= $('.jsClipboardContainer').each($.proxy(function(index
, container
) {
863 this._initContainer(container
);
866 // loads marked items
867 if (this._hasMarkedItems
) {
868 this._loadMarkedItems();
872 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Clipboard', function() {
873 self
._containers
= $('.jsClipboardContainer').each($.proxy(function(index
, container
) {
874 self
._initContainer(container
);
880 * Loads marked items on init.
882 _loadMarkedItems: function() {
883 new WCF
.Action
.Proxy({
886 containerData
: this._containerData
,
887 pageClassName
: this._page
889 success
: $.proxy(this._loadMarkedItemsSuccess
, this),
890 url
: 'index.php/ClipboardLoadMarkedItems/?t=' + SECURITY_TOKEN
+ SID_ARG_2ND
895 * Reloads the list of marked items.
898 this._loadMarkedItems();
902 * Marks all returned items as marked
905 * @param string textStatus
906 * @param jQuery jqXHR
908 _loadMarkedItemsSuccess: function(data
, textStatus
, jqXHR
) {
909 this._resetMarkings();
911 for (var $typeName
in data
.markedItems
) {
912 var $objectData
= data
.markedItems
[$typeName
];
913 for (var $i
in $objectData
) {
914 this._markedObjectIDs
.push($objectData
[$i
]);
917 // loop through all containers
918 this._containers
.each($.proxy(function(index
, container
) {
919 var $container
= $(container
);
921 // typeName does not match, continue
922 if ($container
.data('type') != $typeName
) {
926 // mark items as marked
927 $container
.find('input.jsClipboardItem').each($.proxy(function(innerIndex
, item
) {
929 if (WCF
.inArray($item
.data('objectID'), this._markedObjectIDs
)) {
930 $item
.attr('checked', 'checked');
932 // add marked class for element container
933 $item
.parents('.jsClipboardObject').addClass('jsMarked');
937 // check if there is a markAll-checkbox
938 $container
.find('input.jsClipboardMarkAll').each(function(innerIndex
, markAll
) {
939 var $allItemsMarked
= true;
941 $container
.find('input.jsClipboardItem').each(function(itemIndex
, item
) {
943 if (!$item
.attr('checked')) {
944 $allItemsMarked
= false;
948 if ($allItemsMarked
) {
949 $(markAll
).attr('checked', 'checked');
955 // call success method to build item list editors
956 this._success(data
, textStatus
, jqXHR
);
960 * Resets all checkboxes.
962 _resetMarkings: function() {
963 this._containers
.each(function(index
, container
) {
964 var $container
= $(container
);
966 $container
.find('input.jsClipboardItem, input.jsClipboardMarkAll').removeAttr('checked');
967 $container
.find('.jsClipboardObject').removeClass('jsMarked');
972 * Initializes a clipboard container.
974 * @param object container
976 _initContainer: function(container
) {
977 var $container
= $(container
);
978 var $containerID
= $container
.wcfIdentify();
980 if (!this._trackedElements
[$containerID
]) {
981 $container
.find('.jsClipboardMarkAll').data('hasContainer', $containerID
).click($.proxy(this._markAll
, this));
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
;
990 this._trackedElements
[$containerID
] = [ ];
993 // track individual checkboxes
994 $container
.find('input.jsClipboardItem').each($.proxy(function(index
, input
) {
995 var $input
= $(input
);
996 var $inputID
= $input
.wcfIdentify();
998 if (!WCF
.inArray($inputID
, this._trackedElements
[$containerID
])) {
999 this._trackedElements
[$containerID
].push($inputID
);
1001 $input
.data('hasContainer', $containerID
).click($.proxy(this._click
, this));
1007 * Processes change checkbox state.
1009 * @param object event
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
];
1018 this._markedObjectIDs
.push($objectID
);
1019 $item
.parents('.jsClipboardObject').addClass('jsMarked');
1022 this._markedObjectIDs
= $.removeArrayValue(this._markedObjectIDs
, $objectID
);
1023 $item
.parents('.jsClipboardObject').removeClass('jsMarked');
1026 // item is part of a container
1027 if ($item
.data('hasContainer')) {
1028 var $container
= $('#' + $item
.data('hasContainer'));
1029 var $type
= $container
.data('type');
1031 // check if all items are marked
1032 var $markedAll
= true;
1033 $container
.find('input.jsClipboardItem').each(function(index
, containerItem
) {
1034 var $containerItem
= $(containerItem
);
1035 if (!$containerItem
.attr('checked')) {
1040 // simulate a ticked 'markAll' checkbox
1041 $container
.find('.jsClipboardMarkAll').each(function(index
, markAll
) {
1043 $(markAll
).attr('checked', 'checked');
1046 $(markAll
).removeAttr('checked');
1052 var $type
= $item
.data('type');
1055 this._saveState($type
, $objectIDs
, $isMarked
);
1059 * Marks all associated clipboard items as checked.
1061 * @param object event
1063 _markAll: function(event
) {
1064 var $item
= $(event
.target
);
1065 var $objectIDs
= [ ];
1066 var $isMarked
= true;
1068 // if markAll object is a checkbox, allow toggling
1069 if ($item
.getTagName() == 'input') {
1070 $isMarked
= $item
.attr('checked');
1073 // handle item containers
1074 if ($item
.data('hasContainer')) {
1075 var $container
= $('#' + $item
.data('hasContainer'));
1076 var $type
= $container
.data('type');
1078 // toggle state for all associated items
1079 $container
.find('input.jsClipboardItem').each($.proxy(function(index
, containerItem
) {
1080 var $containerItem
= $(containerItem
);
1081 var $objectID
= $containerItem
.data('objectID');
1083 if (!$containerItem
.attr('checked')) {
1084 $containerItem
.attr('checked', 'checked');
1085 this._markedObjectIDs
.push($objectID
);
1086 $objectIDs
.push($objectID
);
1090 if ($containerItem
.attr('checked')) {
1091 $containerItem
.removeAttr('checked');
1092 this._markedObjectIDs
= $.removeArrayValue(this._markedObjectIDs
, $objectID
);
1093 $objectIDs
.push($objectID
);
1099 $container
.find('.jsClipboardObject').addClass('jsMarked');
1102 $container
.find('.jsClipboardObject').removeClass('jsMarked');
1107 this._saveState($type
, $objectIDs
, $isMarked
);
1111 * Saves clipboard item state.
1113 * @param string type
1114 * @param array objectIDs
1115 * @param boolean isMarked
1117 _saveState: function(type
, objectIDs
, isMarked
) {
1118 this._proxy
.setOption('data', {
1119 action
: (isMarked
) ? 'mark' : 'unmark',
1120 containerData
: this._containerData
,
1121 objectIDs
: objectIDs
,
1122 pageClassName
: this._page
,
1125 this._proxy
.sendRequest();
1129 * Updates editor options.
1131 * @param object data
1132 * @param string textStatus
1133 * @param jQuery jqXHR
1135 _success: function(data
, textStatus
, jqXHR
) {
1136 // clear all editors first
1137 var $containers
= {};
1138 $('.jsClipboardEditor').each(function(index
, container
) {
1139 var $container
= $(container
);
1140 var $types
= eval($container
.data('types'));
1141 for (var $i
= 0, $length
= $types
.length
; $i
< $length
; $i
++) {
1142 var $typeName
= $types
[$i
];
1143 $containers
[$typeName
] = $container
;
1146 var $containerID
= $container
.wcfIdentify();
1147 WCF
.CloseOverlayHandler
.removeCallback($containerID
);
1152 // do not build new editors
1153 if (!data
.items
) return;
1156 for (var $typeName
in data
.items
) {
1157 if (!$containers
[$typeName
]) {
1162 var $container
= $containers
[$typeName
];
1163 var $list
= $container
.children('ul');
1164 if ($list
.length
== 0) {
1165 $list
= $('<ul class="dropdown"></ul>').appendTo($container
);
1168 var $editor
= data
.items
[$typeName
];
1169 var $label
= $('<li><span class="dropdownToggle button">' + $editor
.label
+ '</span></li>').appendTo($list
);
1170 var $itemList
= $('<ol class="dropdownMenu"></ol>').appendTo($label
);
1172 $label
.click(function() { $list
.toggleClass('dropdownOpen'); });
1174 // create editor items
1175 for (var $itemIndex
in $editor
.items
) {
1176 var $item
= $editor
.items
[$itemIndex
];
1178 if ($item
.actionName
=== 'unmarkAll') {
1179 $('<li class="dropdownDivider" />').appendTo($itemList
);
1182 var $listItem
= $('<li><span>' + $item
.label
+ '</span></li>').appendTo($itemList
);
1183 $listItem
.data('objectType', $typeName
);
1184 $listItem
.data('actionName', $item
.actionName
).data('parameters', $item
.parameters
);
1185 $listItem
.data('internalData', $item
.internalData
).data('url', $item
.url
).data('type', $typeName
);
1188 $listItem
.click($.proxy(this._executeAction
, this));
1191 // block click event
1192 $container
.click(function(event
) {
1193 event
.stopPropagation();
1196 // register event handler
1197 var $containerID
= $container
.wcfIdentify();
1198 WCF
.CloseOverlayHandler
.addCallback($containerID
, $.proxy(this._closeLists
, this));
1203 * Closes the clipboard editor item list.
1205 _closeLists: function() {
1206 $('.jsClipboardEditor ul').removeClass('dropdownOpen');
1210 * Executes a clipboard editor item action.
1212 * @param object event
1214 _executeAction: function(event
) {
1215 var $listItem
= $(event
.currentTarget
);
1216 var $url
= $listItem
.data('url');
1218 window
.location
.href
= $url
;
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
);
1228 WCF
.System
.Confirmation
.show($confirmMessage
, $.proxy(function(action
) {
1229 if (action
=== 'confirm') {
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();
1239 this._executeAJAXActions($listItem
, $data
);
1241 }, this), '', $template
);
1244 this._executeAJAXActions($listItem
, { });
1250 $listItem
.trigger('clipboardAction', [ $listItem
.data('type'), $listItem
.data('actionName'), $listItem
.data('parameters') ]);
1254 * Executes the AJAX actions for the given editor list item.
1256 * @param jQuery listItem
1257 * @param object data
1259 _executeAJAXActions: function(listItem
, data
) {
1261 var $objectIDs
= [];
1262 if (listItem
.data('parameters').actionName
!== 'unmarkAll') {
1263 $.each(listItem
.data('parameters').objectIDs
, function(index
, objectID
) {
1264 $objectIDs
.push(parseInt(objectID
));
1270 containerData
: this._containerData
[listItem
.data('type')]
1272 var $__parameters
= listItem
.data('internalData')['parameters'];
1273 if ($__parameters
!== undefined) {
1274 for (var $key
in $__parameters
) {
1275 $parameters
[$key
] = $__parameters
[$key
];
1279 new WCF
.Action
.Proxy({
1282 actionName
: listItem
.data('parameters').actionName
,
1283 className
: listItem
.data('parameters').className
,
1284 objectIDs
: $objectIDs
,
1285 parameters
: $parameters
1287 success
: $.proxy(function(data
) {
1288 if (listItem
.data('parameters').actionName
!== 'unmarkAll') {
1289 listItem
.trigger('clipboardActionResponse', [ data
, listItem
.data('type'), listItem
.data('actionName'), listItem
.data('parameters') ]);
1292 this._loadMarkedItems();
1296 if (this._actionObjects
[listItem
.data('objectType')] && this._actionObjects
[listItem
.data('objectType')][listItem
.data('parameters').actionName
]) {
1297 this._actionObjects
[listItem
.data('objectType')][listItem
.data('parameters').actionName
].triggerEffect($objectIDs
);
1302 * Sends a clipboard proxy request.
1304 * @param object item
1306 sendRequest: function(item
) {
1307 var $item
= $(item
);
1309 this._actionProxy
.setOption('data', {
1310 parameters
: $item
.data('parameters'),
1311 typeName
: $item
.data('type')
1313 this._actionProxy
.sendRequest();
1318 * Provides a simple call for periodical executed functions. Based upon
1319 * ideas by Prototype's PeriodicalExecuter.
1321 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/periodical_executer.js
1322 * @param function callback
1323 * @param integer delay
1325 WCF
.PeriodicalExecuter
= Class
.extend({
1327 * callback for each execution cycle
1342 _isExecuting
: false,
1345 * Initializes a periodical executer.
1347 * @param function callback
1348 * @param integer delay
1350 init: function(callback
, delay
) {
1351 if (!$.isFunction(callback
)) {
1352 console
.debug('[WCF.PeriodicalExecuter] Given callback is invalid, aborting.');
1356 this._callback
= callback
;
1357 this._intervalID
= setInterval($.proxy(this._execute
, this), delay
);
1361 * Executes callback.
1363 _execute: function() {
1364 if (!this._isExecuting
) {
1366 this._isExecuting
= true;
1367 this._callback(this);
1368 this._isExecuting
= false;
1371 this._isExecuting
= false;
1381 if (!this._intervalID
) {
1385 clearInterval(this._intervalID
);
1390 * Namespace for AJAXProxies
1395 * Basic implementation for AJAX-based proxyies
1397 * @param object options
1399 WCF
.Action
.Proxy
= Class
.extend({
1401 * count of active requests
1410 _loadingOverlay
: null,
1413 * loading overlay state
1416 _loadingOverlayVisible
: false,
1419 * timer for overlay activity
1422 _loadingOverlayVisibleTimer
: 0,
1428 _suppressErrors
: false,
1431 * Initializes AJAXProxy.
1433 * @param object options
1435 init: function(options
) {
1436 // initialize default values
1437 this.options
= $.extend(true, {
1443 showLoadingOverlay
: true,
1446 url
: 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN
+ SID_ARG_2ND
1449 this.confirmationDialog
= null;
1450 this.loading
= null;
1451 this._suppressErrors
= false;
1453 // send request immediately after initialization
1454 if (this.options
.autoSend
) {
1459 $(window
).on('beforeunload', function() { self
._suppressErrors
= true; });
1463 * Sends an AJAX request.
1465 sendRequest: function() {
1469 data
: this.options
.data
,
1471 type
: this.options
.type
,
1472 url
: this.options
.url
,
1473 success
: $.proxy(this._success
, this),
1474 error
: $.proxy(this._failure
, this)
1479 * Fires before request is send, displays global loading status.
1482 if ($.isFunction(this.options
.init
)) {
1483 this.options
.init(this);
1486 this._activeRequests
++;
1488 if (this.options
.showLoadingOverlay
) {
1489 this._showLoadingOverlay();
1494 * Displays the loading overlay if not already visible due to an active request.
1496 _showLoadingOverlay: function() {
1497 // create loading overlay on first run
1498 if (this._loadingOverlay
=== null) {
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'));
1503 if (!this._loadingOverlayVisible
) {
1504 this._loadingOverlayVisible
= true;
1505 this._loadingOverlay
.stop(true, true).fadeIn(100, $.proxy(function() {
1506 new WCF
.PeriodicalExecuter($.proxy(this._hideLoadingOverlay
, this), 100);
1512 * Hides loading overlay if no requests are active and the timer reached at least 1 second.
1516 _hideLoadingOverlay: function(pe
) {
1517 this._loadingOverlayVisibleTimer
+= 100;
1519 if (this._activeRequests
== 0 && this._loadingOverlayVisibleTimer
>= 100) {
1520 this._loadingOverlayVisible
= false;
1521 this._loadingOverlayVisibleTimer
= 0;
1524 this._loadingOverlay
.fadeOut(100);
1529 * Handles AJAX errors.
1531 * @param object jqXHR
1532 * @param string textStatus
1533 * @param string errorThrown
1535 _failure: function(jqXHR
, textStatus
, errorThrown
) {
1537 var data
= $.parseJSON(jqXHR
.responseText
);
1539 // call child method if applicable
1540 var $showError
= true;
1541 if ($.isFunction(this.options
.failure
)) {
1542 $showError
= this.options
.failure(jqXHR
, textStatus
, errorThrown
, jqXHR
.responseText
);
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') });
1549 // failed to parse JSON
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
);
1557 if (!this._suppressErrors
&& $showError
!== false) {
1558 $('<div class="ajaxDebugMessage"><p>' + jqXHR
.responseText
+ '</p></div>').wcfDialog({ title
: WCF
.Language
.get('wcf.global.error.title') });
1566 * Handles successful AJAX requests.
1568 * @param object data
1569 * @param string textStatus
1570 * @param object jqXHR
1572 _success: function(data
, textStatus
, jqXHR
) {
1573 // enable DOMNodeInserted event
1574 WCF
.DOMNodeInsertedHandler
.enable();
1576 // call child method if applicable
1577 if ($.isFunction(this.options
.success
)) {
1578 this.options
.success(data
, textStatus
, jqXHR
);
1585 * Fires after an AJAX request, hides global loading status.
1587 _after: function() {
1588 if ($.isFunction(this.options
.after
)) {
1589 this.options
.after();
1592 this._activeRequests
--;
1594 // disable DOMNodeInserted event
1595 WCF
.DOMNodeInsertedHandler
.disable();
1599 * Sets options, MUST be used to set parameters before sending request
1600 * if calling from child classes.
1602 * @param string optionName
1603 * @param mixed optionData
1605 setOption: function(optionName
, optionData
) {
1606 this.options
[optionName
] = optionData
;
1610 * Displays a spinner image for given element.
1612 * @param jQuery element
1614 showSpinner: function(element
) {
1615 element
= $(element
);
1617 if (element
.getTagName() !== 'img') {
1618 console
.debug('The given element is not an image, aborting.');
1622 // force element dimensions
1623 element
.attr('width', element
.attr('width'));
1624 element
.attr('height', element
.attr('height'));
1627 element
.attr('src', WCF
.Icon
.get('wcf.global.loading'));
1632 * Basic implementation for simple proxy access using bound elements.
1634 * @param object options
1635 * @param object callbacks
1637 WCF
.Action
.SimpleProxy
= Class
.extend({
1639 * Initializes SimpleProxy.
1641 * @param object options
1642 * @param object callbacks
1644 init: function(options
, callbacks
) {
1646 * action-specific options
1648 this.options
= $.extend(true, {
1656 * proxy-specific options
1658 this.callbacks
= $.extend(true, {
1665 if (!this.options
.elements
) return;
1668 this.proxy
= new WCF
.Action
.Proxy(this.callbacks
);
1670 // bind event listener
1671 this.options
.elements
.each($.proxy(function(index
, element
) {
1672 $(element
).bind(this.options
.eventName
, $.proxy(this._handleEvent
, this));
1677 * Handles event actions.
1679 * @param object event
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') ]
1688 this.proxy
.sendRequest();
1693 * Basic implementation for AJAXProxy-based deletion.
1695 * @param string className
1696 * @param string containerSelector
1698 WCF
.Action
.Delete
= Class
.extend({
1706 * container selector
1709 _containerSelector
: '',
1712 * list of known container ids
1713 * @var array<string>
1718 * Initializes 'delete'-Proxy.
1720 * @param string className
1721 * @param string containerSelector
1723 init: function(className
, containerSelector
) {
1724 this._containerSelector
= containerSelector
;
1725 this._className
= className
;
1726 this.proxy
= new WCF
.Action
.Proxy({
1727 success
: $.proxy(this._success
, this)
1730 this._initElements();
1732 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Action.Delete' + this._className
.hashCode(), $.proxy(this._initElements
, this));
1736 * Initializes available element containers.
1738 _initElements: function() {
1740 $(this._containerSelector
).each(function(index
, container
) {
1741 var $container
= $(container
);
1742 var $containerID
= $container
.wcfIdentify();
1744 if (!WCF
.inArray($containerID
, self
._containers
)) {
1745 self
._containers
.push($containerID
);
1746 $container
.find('.jsDeleteButton').click($.proxy(self
._click
, self
));
1752 * Sends AJAX request.
1754 * @param object event
1756 _click: function(event
) {
1757 var $target
= $(event
.currentTarget
);
1759 if ($target
.data('confirmMessage')) {
1760 WCF
.System
.Confirmation
.show($target
.data('confirmMessage'), $.proxy(this._execute
, this), { target
: $target
});
1763 this.proxy
.showSpinner($target
);
1764 this._sendRequest($target
);
1769 * Executes deletion.
1771 * @param string action
1772 * @param object parameters
1774 _execute: function(action
, parameters
) {
1775 if (action
=== 'cancel') {
1779 this.proxy
.showSpinner(parameters
.target
);
1780 this._sendRequest(parameters
.target
);
1783 _sendRequest: function(object
) {
1784 this.proxy
.setOption('data', {
1785 actionName
: 'delete',
1786 className
: this._className
,
1787 objectIDs
: [ $(object
).data('objectID') ]
1790 this.proxy
.sendRequest();
1794 * Deletes items from containers.
1796 * @param object data
1797 * @param string textStatus
1798 * @param object jqXHR
1800 _success: function(data
, textStatus
, jqXHR
) {
1801 this.triggerEffect(data
.objectIDs
);
1805 * Triggers the delete effect for the objects with the given ids.
1807 * @param array objectIDs
1809 triggerEffect: function(objectIDs
) {
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(); });
1820 * Basic implementation for AJAXProxy-based toggle actions.
1822 * @param string className
1823 * @param jQuery containerList
1824 * @param string toggleButtonSelector
1826 WCF
.Action
.Toggle
= Class
.extend({
1828 * Initializes 'toggle'-Proxy
1830 * @param string className
1831 * @param jQuery containerList
1833 init: function(className
, containerList
, toggleButtonSelector
) {
1834 if (!containerList
.length
) return;
1835 this.containerList
= containerList
;
1836 this.className
= className
;
1838 this.toggleButtonSelector
= '.jsToggleButton';
1839 if (toggleButtonSelector
) {
1840 this.toggleButtonSelector
= toggleButtonSelector
;
1845 success
: $.proxy(this._success
, this)
1847 this.proxy
= new WCF
.Action
.Proxy(options
);
1849 // bind event listener
1850 this.containerList
.each($.proxy(function(index
, container
) {
1851 $(container
).find(this.toggleButtonSelector
).bind('click', $.proxy(this._click
, this));
1856 * Sends AJAX request.
1858 * @param object event
1860 _click: function(event
) {
1861 this.proxy
.setOption('data', {
1862 actionName
: 'toggle',
1863 className
: this.className
,
1864 objectIDs
: [ $(event
.target
).data('objectID') ]
1867 this.proxy
.sendRequest();
1871 * Toggles status icons.
1873 * @param object data
1874 * @param string textStatus
1875 * @param object jqXHR
1877 _success: function(data
, textStatus
, jqXHR
) {
1878 this.triggerEffect(data
.objectIDs
);
1882 * Triggers the toggle effect for the objects with the given ids.
1884 * @param array objectIDs
1886 triggerEffect: function(objectIDs
) {
1887 this.containerList
.each($.proxy(function(index
, container
) {
1888 var $toggleButton
= $(container
).find(this.toggleButtonSelector
);
1889 if (WCF
.inArray($toggleButton
.data('objectID'), objectIDs
)) {
1890 $(container
).wcfHighlight();
1892 // toggle icon source
1893 $toggleButton
.attr('src', function() {
1894 if (this.src
.match(/disabled\.svg$/)) {
1895 return this.src
.replace(/disabled\.svg$/, 'enabled.svg');
1898 return this.src
.replace(/enabled\.svg$/, 'disabled.svg');
1902 // toogle icon title
1903 $toggleButton
.attr('title', function() {
1904 if (this.src
.match(/enabled\.svg$/)) {
1905 if ($(this).data('disableTitle')) {
1906 return $(this).data('disableTitle');
1909 return WCF
.Language
.get('wcf.global.button.disable');
1912 if ($(this).data('enableTitle')) {
1913 return $(this).data('enableTitle');
1916 return WCF
.Language
.get('wcf.global.button.enable');
1921 $(container
).toggleClass('disabled');
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.
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.
1934 * @param integer threshold
1935 * @param object callback
1936 * @param string reference
1937 * @param string target
1939 WCF
.Action
.Scroll
= Class
.extend({
1941 * callback used once threshold is reached
1965 * Initializes a new WCF.Action.Scroll object.
1967 * @param integer threshold
1968 * @param object callback
1969 * @param string reference
1970 * @param string target
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.");
1979 if ($.isFunction(callback
)) this._callback
= callback
;
1980 if (this._callback
=== null) {
1981 console
.debug("[WCF.Action.Scroll] Given callback is invalid, aborting.");
1985 // bind element references
1986 this._reference
= $((reference
) ? reference
: window
);
1987 this._target
= $((target
) ? target
: document
);
1989 // watch for scroll event
1994 * Calculates if threshold is reached and notifies callback.
1996 _scroll: function() {
1997 var $targetHeight
= this._target
.height();
1998 var $topOffset
= this._reference
.scrollTop();
1999 var $referenceHeight
= this._reference
.height();
2001 // calculate if defined threshold is visible
2002 if (($targetHeight
- ($referenceHeight
+ $topOffset
)) < this._threshold
) {
2003 this._callback(this);
2008 * Enables scroll monitoring, may be used to resume.
2011 this._reference
.on('scroll', $.proxy(this._scroll
, this));
2015 * Disables scroll monitoring, e.g. no more elements loadable.
2018 this._reference
.off('scroll');
2023 * Namespace for date-related functions.
2028 * Provides a date picker for date input fields.
2032 * Initializes the jQuery UI based date picker.
2035 $('input[type=date]').each(function(index
, input
) {
2036 // do *not* use .attr()
2037 var $input
= $(input
).prop('type', 'text');
2039 // TODO: we should support all these braindead date formats, at least within output
2043 showOtherMonths
: true,
2044 dateFormat
: 'yy-mm-dd',
2045 yearRange
: '1900:2038' // TODO: make it configurable?
2052 * Provides utility functions for date operations.
2056 * Returns UTC timestamp, if date is not given, current time will be used.
2061 gmdate: function(date
) {
2062 var $date
= (date
) ? date
: new Date();
2064 return Math
.round(Date
.UTC(
2065 $date
.getUTCFullYear(),
2066 $date
.getUTCMonth(),
2068 $date
.getUTCHours(),
2069 $date
.getUTCMinutes(),
2070 $date
.getUTCSeconds()
2075 * Returns a Date object with precise offset (including timezone and local timezone).
2076 * Parameter timestamp must be in miliseconds!
2078 * @param integer timestamp
2079 * @param integer offset
2082 getTimezoneDate: function(timestamp
, offset
) {
2083 var $date
= new Date(timestamp
);
2084 var $localOffset
= $date
.getTimezoneOffset() * -1 * 60000;
2086 return new Date((timestamp
- $localOffset
- offset
));
2091 * Handles relative time designations.
2093 WCF
.Date
.Time
= Class
.extend({
2095 * Initializes relative datetimes.
2098 // initialize variables
2099 this.elements
= $('time.datetime');
2102 // calculate relative datetime on init
2105 // re-calculate relative datetime every minute
2106 new WCF
.PeriodicalExecuter($.proxy(this._refresh
, this), 60000);
2108 // bind dom node inserted listener
2109 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Date.Time', $.proxy(this._domNodeInserted
, this));
2113 * Updates element collection once a DOM node was inserted.
2115 _domNodeInserted: function() {
2116 this.elements
= $('time.datetime');
2121 * Refreshes relative datetime for each element.
2123 _refresh: function() {
2125 var $date
= new Date();
2126 this.timestamp
= ($date
.getTime() - $date
.getMilliseconds()) / 1000;
2129 this.elements
.each($.proxy(this._refreshElement
, this));
2133 * Refreshes relative datetime for current element.
2135 * @param integer index
2136 * @param object element
2138 _refreshElement: function(index
, element
) {
2139 if (!$(element
).attr('title')) {
2140 $(element
).attr('title', $(element
).text());
2143 var $timestamp
= $(element
).data('timestamp');
2144 var $date
= $(element
).data('date');
2145 var $time
= $(element
).data('time');
2146 var $offset
= $(element
).data('offset');
2148 // timestamp is in the future
2149 if ($timestamp
> this.timestamp
) {
2150 var $string
= WCF
.Language
.get('wcf.date.dateTimeFormat');
2151 $(element
).text($string
.replace(/\%date\%/, $date
).replace(/\%time\%/, $time
));
2153 // timestamp is less than 60 minutes ago (display 1 hour ago rather than 60 minutes ago)
2154 else if (this.timestamp
< ($timestamp
+ 3540)) {
2155 var $minutes
= Math
.round((this.timestamp
- $timestamp
) / 60);
2156 $(element
).text(eval(WCF
.Language
.get('wcf.date.relative.minutes')));
2158 // timestamp is less than 24 hours ago
2159 else if (this.timestamp
< ($timestamp
+ 86400)) {
2160 var $hours
= Math
.round((this.timestamp
- $timestamp
) / 3600);
2161 $(element
).text(eval(WCF
.Language
.get('wcf.date.relative.hours')));
2163 // timestamp is less than a week ago
2164 else if (this.timestamp
< ($timestamp
+ 604800)) {
2165 var $days
= Math
.round((this.timestamp
- $timestamp
) / 86400);
2166 var $string
= eval(WCF
.Language
.get('wcf.date.relative.pastDays'));
2169 var $dateObj
= WCF
.Date
.Util
.getTimezoneDate(($timestamp
* 1000), $offset
);
2170 var $dow
= $dateObj
.getDay();
2172 $(element
).text($string
.replace(/\%day\%/, WCF
.Language
.get('__days')[$dow
]).replace(/\%time\%/, $time
));
2174 // timestamp is between ~700 million years BC and last week
2176 var $string
= WCF
.Language
.get('wcf.date.dateTimeFormat');
2177 $(element
).text($string
.replace(/\%date\%/, $date
).replace(/\%time\%/, $time
));
2183 * Hash-like dictionary. Based upon idead from Prototype's hash
2185 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/hash.js
2187 WCF
.Dictionary
= Class
.extend({
2195 * Initializes a new dictionary.
2198 this._variables
= { };
2205 * @param mixed value
2207 add: function(key
, value
) {
2208 this._variables
[key
] = value
;
2212 * Adds a traditional object to current dataset.
2214 * @param object object
2216 addObject: function(object
) {
2217 for (var $key
in object
) {
2218 this.add($key
, object
[$key
]);
2223 * Adds a dictionary to current dataset.
2225 * @param object dictionary
2227 addDictionary: function(dictionary
) {
2228 dictionary
.each($.proxy(function(pair
) {
2229 this.add(pair
.key
, pair
.value
);
2234 * Retrieves the value of an entry or returns null if key is not found.
2239 get: function(key
) {
2240 if (this.isset(key
)) {
2241 return this._variables
[key
];
2248 * Returns true if given key is a valid entry.
2252 isset: function(key
) {
2253 return this._variables
.hasOwnProperty(key
);
2261 remove: function(key
) {
2262 delete this._variables
[key
];
2266 * Iterates through dictionary.
2269 * var $hash = new WCF.Dictionary();
2270 * $hash.add('foo', 'bar');
2271 * $hash.each(function(pair) {
2272 * // alerts: foo = bar
2273 * alert(pair.key + ' = ' + pair.value);
2276 * @param function callback
2278 each: function(callback
) {
2279 if (!$.isFunction(callback
)) {
2283 for (var $key
in this._variables
) {
2284 var $value
= this._variables
[$key
];
2295 * Returns the amount of items.
2300 return $.getLength(this._variables
);
2304 * Returns true, if dictionary is empty.
2308 isEmpty: function() {
2309 return !this.count();
2314 * Global language storage.
2316 * @see WCF.Dictionary
2319 _variables
: new WCF
.Dictionary(),
2322 * @see WCF.Dictionary.add()
2324 add: function(key
, value
) {
2325 this._variables
.add(key
, value
);
2329 * @see WCF.Dictionary.addObject()
2331 addObject: function(object
) {
2332 this._variables
.addObject(object
);
2336 * Retrieves a variable.
2341 get: function(key
, parameters
) {
2342 // initialize parameters with an empty object
2343 if (typeof parameters
=== 'undefined') var parameters
= {};
2345 var value
= this._variables
.get(key
);
2347 if (typeof value
=== 'string') {
2348 // transform strings into template and try to refetch
2349 this.add(key
, new WCF
.Template(value
));
2350 return this.get(key
, parameters
);
2352 else if (value
!== null && typeof value
=== 'object' && typeof value
.fetch
!== 'undefined') {
2353 // evaluate templates
2354 value
= value
.fetch(parameters
);
2356 else if (value
=== null) {
2366 * Handles multiple language input fields.
2368 * @param string elementID
2369 * @param boolean forceSelection
2370 * @param object values
2371 * @param object availableLanguages
2373 WCF
.MultipleLanguageInput
= Class
.extend({
2375 * list of available languages
2378 _availableLanguages
: {},
2381 * initialization state
2387 * target input element
2393 * true, if data was entered after initialization
2396 _insertedDataAfterInit
: false,
2399 * enables multiple language ability
2405 * enforce multiple language ability
2408 _forceSelection
: false,
2411 * currently active language id
2417 * language selection list
2423 * list of language values on init
2429 * Initializes multiple language ability for given element id.
2431 * @param integer elementID
2432 * @param boolean forceSelection
2433 * @param boolean isEnabled
2434 * @param object values
2435 * @param object availableLanguages
2437 init: function(elementID
, forceSelection
, values
, availableLanguages
) {
2438 this._element
= $('#' + $.wcfEscapeID(elementID
));
2439 this._forceSelection
= forceSelection
;
2440 this._values
= values
;
2441 this._availableLanguages
= availableLanguages
;
2443 // default to current user language
2444 this._languageID
= LANGUAGE_ID
;
2445 if (this._element
.length
== 0) {
2446 console
.debug("[WCF.MultipleLanguageInput] element id '" + elementID
+ "' is unknown");
2450 // build selection handler
2451 var $enableOnInit
= ($.getLength(this._values
) > 0) ? true : false;
2452 this._insertedDataAfterInit
= $enableOnInit
;
2453 this._prepareElement($enableOnInit
);
2455 // listen for submit event
2456 this._element
.parents('form').submit($.proxy(this._submit
, this));
2458 this._didInit
= true;
2462 * Builds language handler.
2464 * @param boolean enableOnInit
2466 _prepareElement: function(enableOnInit
) {
2467 // enable DOMNodeInserted event
2468 WCF
.DOMNodeInsertedHandler
.enable();
2470 this._element
.wrap('<div class="dropdown preInput" />');
2471 var $wrapper
= this._element
.parent();
2472 var $button
= $('<p class="button dropdownToggle"><span>' + WCF
.Language
.get('wcf.global.button.disabledI18n') + '</span></p>').prependTo($wrapper
);
2473 $button
.data('target', $wrapper
.wcfIdentify()).click($.proxy(this._enable
, this));
2476 this._list
= $('<ul class="dropdownMenu"></ul>').insertAfter($button
);
2478 // add a special class if next item is a textarea
2479 if ($button
.nextAll('textarea').length
) {
2480 $button
.addClass('dropdownCaptionTextarea');
2483 $button
.addClass('dropdownCaption');
2486 // insert available languages
2487 for (var $languageID
in this._availableLanguages
) {
2488 $('<li><span>' + this._availableLanguages
[$languageID
] + '</span></li>').data('languageID', $languageID
).click($.proxy(this._changeLanguage
, this)).appendTo(this._list
);
2491 // disable language input
2492 if (!this._forceSelection
) {
2493 $('<li class="dropdownDivider" />').appendTo(this._list
);
2494 $('<li><span>' + WCF
.Language
.get('wcf.global.button.disabledI18n') + '</span></li>').click($.proxy(this._disable
, this)).appendTo(this._list
);
2497 if (enableOnInit
|| this._forceSelection
) {
2498 $button
.trigger('click');
2500 // pre-select current language
2501 this._list
.children('li').each($.proxy(function(index
, listItem
) {
2502 var $listItem
= $(listItem
);
2503 if ($listItem
.data('languageID') == this._languageID
) {
2504 $listItem
.trigger('click');
2509 WCF
.Dropdown
.registerCallback($wrapper
.wcfIdentify(), $.proxy(this._handleAction
, this));
2511 // disable DOMNodeInserted event
2512 WCF
.DOMNodeInsertedHandler
.disable();
2516 * Handles dropdown actions.
2518 * @param jQuery dropdown
2519 * @param string action
2521 _handleAction: function(dropdown
, action
) {
2522 if (action
=== 'close') {
2523 this._closeSelection();
2528 * Enables the language selection or shows the selection if already enabled.
2530 * @param object event
2532 _enable: function(event
) {
2533 if (!this._isEnabled
) {
2534 var $button
= $(event
.currentTarget
);
2535 $button
.next('.dropdownMenu').css({
2536 top
: ($button
.outerHeight() - 1) + 'px'
2539 if ($button
.getTagName() === 'p') {
2540 $button
= $button
.children('span:eq(0)');
2543 $button
.addClass('active');
2545 this._isEnabled
= true;
2549 if (this._list
.is(':visible')) {
2550 this._showSelection();
2554 event
.stopPropagation();
2558 * Shows the language selection.
2560 _showSelection: function() {
2561 if (this._isEnabled
) {
2562 // display status for each language
2563 this._list
.children('li').each($.proxy(function(index
, listItem
) {
2564 var $listItem
= $(listItem
);
2565 var $languageID
= $listItem
.data('languageID');
2568 if (this._values
[$languageID
] && this._values
[$languageID
] != '') {
2569 $listItem
.removeClass('missingValue');
2572 $listItem
.addClass('missingValue');
2580 * Closes the language selection.
2582 _closeSelection: function() {
2587 * Changes the currently active language.
2589 * @param object event
2591 _changeLanguage: function(event
) {
2592 var $button
= $(event
.currentTarget
);
2593 this._insertedDataAfterInit
= true;
2595 // save current value
2596 if (this._didInit
) {
2597 this._values
[this._languageID
] = this._element
.val();
2601 this._languageID
= $button
.data('languageID');
2602 if (this._values
[this._languageID
]) {
2603 this._element
.val(this._values
[this._languageID
]);
2606 this._element
.val('');
2610 this._list
.children('li').removeClass('active');
2611 $button
.addClass('active');
2614 this._list
.prev('.dropdownToggle').children('span').text(this._availableLanguages
[this._languageID
]);
2616 // close selection and set focus on input element
2617 //this._closeSelection();
2618 this._element
.blur().focus();
2622 * Disables language selection for current element.
2624 * @param object event
2626 _disable: function(event
) {
2627 if (event
=== undefined && this._insertedDataAfterInit
) {
2631 if (this._forceSelection
|| !this._list
|| event
=== null) {
2635 // remove active marking
2636 this._list
.prev('.dropdownToggle').children('span').removeClass('active').text(WCF
.Language
.get('wcf.global.button.disabledI18n'));
2638 // update element value
2639 if (this._values
[LANGUAGE_ID
]) {
2640 this._element
.val(this._values
[LANGUAGE_ID
]);
2643 // no value for current language found, proceed with empty input
2644 this._element
.val();
2647 this._element
.blur();
2648 this._insertedDataAfterInit
= false;
2649 this._isEnabled
= false;
2654 * Prepares language variables on before submit.
2656 _submit: function() {
2657 // insert hidden form elements on before submit
2658 if (!this._isEnabled
) {
2662 // fetch active value
2663 if (this._languageID
) {
2664 this._values
[this._languageID
] = this._element
.val();
2667 var $form
= $(this._element
.parents('form')[0]);
2668 var $elementID
= this._element
.wcfIdentify();
2670 for (var $languageID
in this._values
) {
2671 $('<input type="hidden" name="' + $elementID
+ '_i18n[' + $languageID
+ ']" value="' + this._values
[$languageID
] + '" />').appendTo($form
);
2674 // remove name attribute to prevent conflict with i18n values
2675 this._element
.removeAttr('name');
2680 * Icon collection used across all JavaScript classes.
2682 * @see WCF.Dictionary
2687 * @var WCF.Dictionary
2689 _icons
: new WCF
.Dictionary(),
2692 * @see WCF.Dictionary.add()
2694 add: function(name
, path
) {
2695 this._icons
.add(name
, path
);
2699 * @see WCF.Dictionary.addObject()
2701 addObject: function(object
) {
2702 this._icons
.addObject(object
);
2706 * @see WCF.Dictionary.get()
2708 get: function(name
) {
2709 return this._icons
.get(name
);
2718 * Rounds a number to a given number of floating points digits. Defaults to 0.
2720 * @param number number
2721 * @param floatingPoint number of digits
2724 round: function (number
, floatingPoint
) {
2725 floatingPoint
= Math
.pow(10, (floatingPoint
|| 0));
2727 return Math
.round(number
* floatingPoint
) / floatingPoint
;
2736 * Adds thousands separators to a given number.
2738 * @param mixed number
2741 addThousandsSeparator: function(number
) {
2742 var $numberString
= String(number
);
2743 var parts
= $numberString
.split(/[^0-9]+/);
2745 var $decimalPoint
= $numberString
.match(/[^0-9]+/);
2747 $numberString
= parts
[0];
2748 var $decimalPart
= '';
2749 if ($decimalPoint
!== null) {
2751 var $decimalPart
= $decimalPoint
.join('')+parts
.join('');
2753 if (parseInt(number
) >= 1000 || parseInt(number
) <= -1000) {
2754 var $negative
= false;
2755 if (parseInt(number
) <= -1000) {
2757 $numberString
= $numberString
.substring(1);
2759 var $separator
= WCF
.Language
.get('wcf.global.thousandsSeparator');
2761 if ($separator
!= null && $separator
!= '') {
2762 var $numElements
= new Array();
2763 var $firstPart
= $numberString
.length
% 3;
2764 if ($firstPart
== 0) $firstPart
= 3;
2765 for (var $i
= 0; $i
< Math
.ceil($numberString
.length
/ 3); $i
++) {
2766 if ($i
== 0) $numElements
.push($numberString
.substring(0, $firstPart
));
2768 var $start
= (($i
- 1) * 3) + $firstPart
;
2769 $numElements
.push($numberString
.substring($start
, $start
+ 3));
2772 $numberString
= (($negative
) ? ('-') : ('')) + $numElements
.join($separator
);
2776 return $numberString
+ $decimalPart
;
2780 * Escapes special HTML-characters within a string
2782 * @param string string
2785 escapeHTML: function (string
) {
2786 return string
.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
2790 * Escapes a String to work with RegExp.
2792 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25
2793 * @param string string
2796 escapeRegExp: function(string) {
2797 return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
2801 * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands-separators
2803 * @param mixed number
2806 formatNumeric: function(number, floatingPoint) {
2807 number = String(WCF.Number.round(number, floatingPoint || 2));
2808 number = number.replace('.', WCF.Language.get('wcf.global.decimalPoint'));
2810 return this.addThousandsSeparator(number);
2814 * Makes a string's first character lowercase
2816 * @param string string
2819 lcfirst: function(string) {
2820 return string.substring(0, 1).toLowerCase() + string.substring(1);
2824 * Makes a string's first character uppercase
2826 * @param string string
2829 ucfirst: function(string) {
2830 return string.substring(0, 1).toUpperCase() + string.substring(1);
2835 * Basic implementation for WCF TabMenus. Use the data attributes 'active' to specify the
2836 * tab which should be shown on init. Furthermore you may specify a 'store' data-attribute
2837 * which will be filled with the currently selected tab.
2841 * list of tabmenu containers
2847 * initialization state
2853 * Initializes all TabMenus
2856 var $containers = $('.tabMenuContainer');
2858 $containers.each(function(index, tabMenu) {
2859 var $tabMenu = $(tabMenu);
2860 var $containerID = $tabMenu.wcfIdentify();
2861 if (self._containers[$containerID]) {
2862 // continue with next container
2866 if ($tabMenu.data('store') && !$('#' + $tabMenu.data('store')).length) {
2867 $('<input type="hidden
" name="' + $tabMenu.data('store
') + '" value="" id="' + $tabMenu.data('store
') + '" />').appendTo($tabMenu.parents('form').find('.formSubmit'));
2870 // init jQuery UI TabMenu
2871 self._containers[$containerID] = $tabMenu;
2873 select: function(event, ui) {
2874 var $panel = $(ui.panel);
2875 var $container = $panel.closest('.tabMenuContainer');
2877 // store currently selected item
2878 var $tabMenu = $container;
2880 // do not trigger on init
2881 if ($tabMenu.data('isParent') === undefined) {
2885 if ($tabMenu.data('isParent')) {
2886 if ($tabMenu.data('store')) {
2887 $('#' + $tabMenu.data('store')).val($panel.attr('id'));
2893 $tabMenu = $tabMenu.data('parent');
2897 // set panel id as location hash
2898 if (WCF.TabMenu._didInit) {
2899 location.hash = '#' + $panel.attr('id');
2902 $container.trigger('tabsselect', event, ui);
2906 $tabMenu.data('isParent', ($tabMenu.children('.tabMenuContainer, .tabMenuContent').length > 0)).data('parent', false);
2907 if (!$tabMenu.data('isParent')) {
2908 // check if we're a child element
2909 if ($tabMenu.parent().hasClass('tabMenuContainer')) {
2910 $tabMenu.data('parent', $tabMenu.parent());
2915 // try to resolve location hash
2916 if (!this._didInit) {
2918 $(window).bind('hashchange', $.proxy(this.selectTabs, this));
2920 if (!this._selectErroneousTab()) {
2921 this._selectActiveTab();
2925 this._didInit = true;
2929 * Force display of first erroneous tab, returns true, if at
2930 * least one tab contains an error.
2934 _selectErroneousTab: function() {
2935 for (var $containerID in this._containers) {
2936 var $tabMenu = this._containers[$containerID];
2938 if (!$tabMenu.data('isParent') && $tabMenu.find('.formError').length) {
2940 if ($tabMenu.data('parent') === false) {
2944 $tabMenu = $tabMenu.data('parent').wcfTabs('select', $tabMenu.wcfIdentify());
2955 * Selects the active tab menu item.
2957 _selectActiveTab: function() {
2958 for (var $containerID in this._containers) {
2959 var $tabMenu = this._containers[$containerID];
2960 if ($tabMenu.data('active')) {
2961 var $index = $tabMenu.data('active');
2962 var $subIndex = null;
2963 if (/-/.test($index)) {
2964 var $tmp = $index.split('-');
2966 $subIndex = $tmp[1];
2969 $tabMenu.find('.tabMenuContent').each(function(innerIndex, tabMenuItem) {
2970 var $tabMenuItem = $(tabMenuItem);
2971 if ($tabMenuItem.wcfIdentify() == $index) {
2972 $tabMenu.wcfTabs('select', innerIndex);
2974 if ($subIndex !== null) {
2975 if ($tabMenuItem.hasClass('tabMenuContainer')) {
2976 $tabMenuItem.wcfTabs('select', $tabMenu.data('active'));
2979 $tabMenu.wcfTabs('select', $tabMenu.data('active'));
2991 * Resolves location hash to display tab menus.
2993 selectTabs: function() {
2994 if (location.hash) {
2995 var $hash = location.hash.substr(1);
2996 var $subIndex = null;
2997 if (/-/.test(location.hash)) {
2998 var $tmp = $hash.split('-');
3000 $subIndex = $tmp[1];
3003 // find a container which matches the first part
3004 for (var $containerID in this._containers) {
3005 var $tabMenu = this._containers[$containerID];
3006 if ($tabMenu.wcfTabs('hasAnchor', $hash, false)) {
3007 if ($subIndex !== null) {
3008 // try to find child tabMenu
3009 var $childTabMenu = $tabMenu.find('#' + $.wcfEscapeID($hash) + '.tabMenuContainer');
3010 if ($childTabMenu.length !== 1) {
3014 // validate match for second part
3015 if (!$childTabMenu.wcfTabs('hasAnchor', $subIndex, true)) {
3019 $childTabMenu.wcfTabs('select', $hash + '-' + $subIndex);
3022 $tabMenu.wcfTabs('select', $hash);
3031 * Templates that may be fetched more than once with different variables.
3032 * Based upon ideas from Prototype's template.
3035 * var myTemplate = new WCF.Template('{$hello} World');
3036 * myTemplate.fetch({ hello: 'Hi' }); // Hi World
3037 * myTemplate.fetch({ hello: 'Hello' }); // Hello World
3039 * my2ndTemplate = new WCF.Template('{@$html}{$html}');
3040 * my2ndTemplate.fetch({ html: '<b>Test</b>' }); // <b>Test</b><b>Test</b>
3042 * var my3rdTemplate = new WCF.Template('You can use {literal}{$variable}{/literal}-Tags here');
3043 * my3rdTemplate.fetch({ variable: 'Not shown' }); // You can use {$variable}-Tags here
3045 * @param template template-content
3046 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/template.js
3048 WCF.Template = Class.extend({
3056 * saved literal tags
3057 * @var WCF.Dictionary
3059 _literals: new WCF.Dictionary(),
3064 * @param $template template-content
3066 init: function($template) {
3067 this._template = $template;
3069 // save literal-tags
3070 this._template = this._template.replace(/\{literal\}(.*?)\{\/literal\}/g, $.proxy(function ($match) {
3071 // hopefully no one uses this string in one of his templates
3072 var id = '@@@@@@@@@@@'+Math.random()+'@@@@@@@@@@@';
3073 this._literals.add(id, $match.replace(/\{\/?literal\}/g, ''));
3080 * Fetches the template with the given variables
3082 * @param $variables variables to insert
3083 * @return parsed template
3085 fetch: function($variables) {
3086 var $result = this._template;
3089 for (var $key in $variables) {
3090 $result = $result.replace(new RegExp(WCF.String.escapeRegExp('{$'+$key+'}'), 'g'), WCF.String.escapeHTML(new String($variables[$key])));
3091 $result = $result.replace(new RegExp(WCF.String.escapeRegExp('{#$'+$key+'}'), 'g'), WCF.String.formatNumeric($variables[$key]));
3092 $result = $result.replace(new RegExp(WCF.String.escapeRegExp('{@$'+$key+'}'), 'g'), $variables[$key]);
3095 // insert delimiter tags
3096 $result = $result.replace('{ldelim}', '{').replace('{rdelim}', '}');
3098 // and re-insert saved literals
3099 return this.insertLiterals($result);
3103 * Inserts literals into given string
3105 * @param $template string to insert into
3106 * @return string with inserted literals
3108 insertLiterals: function ($template) {
3109 this._literals.each(function ($pair) {
3110 $template = $template.replace($pair.key, $pair.value);
3117 * Compiles this template into javascript-code
3119 * @return WCF.Template.Compiled
3121 compile: function () {
3122 var $compiled = this._template;
3125 $compiled = $compiled.replace('\\', '\\\\').replace("'", "\\'");
3127 // parse our variable-tags
3128 $compiled = $compiled.replace(/\{\$(.*?)\}/g, function ($match) {
3129 var $name = '$v.' + $match.substring(2, $match.length - 1);
3130 // trinary operator to maintain compatibility with uncompiled template
3131 // ($name) ? $name : '$match'
3132 // -> $v.muh ? $v.muh : '{$muh}'
3133 return "' + WCF.String.escapeHTML("+ $name + " ? " + $name + " : '" + $match + "') + '";
3134 }).replace(/\{#\$(.*?)\}/g, function ($match) {
3135 var $name = '$v.' + $match.substring(3, $match.length - 1);
3136 // trinary operator to maintain compatibility with uncompiled template
3137 // ($name) ? $name : '$match'
3138 // -> $v.muh ? $v.muh : '{$muh}'
3139 return "' + WCF.String.formatNumeric("+ $name + " ? " + $name + " : '" + $match + "') + '";
3140 }).replace(/\{@\$(.*?)\}/g, function ($match) {
3141 var $name = '$v.' + $match.substring(3, $match.length - 1);
3142 // trinary operator to maintain compatibility with uncompiled template
3143 // ($name) ? $name : '$match'
3144 // -> $v.muh ? $v.muh : '{$muh}'
3145 return "' + ("+ $name + " ? " + $name + " : '" + $match + "') + '";
3148 // insert delimiter tags
3149 $compiled = $compiled.replace('{ldelim}', '{').replace('{rdelim}', '}');
3152 $compiled = $compiled.replace(/(\r\n|\n|\r)/g, '\\n');
3154 // and re-insert saved literals
3155 return new WCF.Template.Compiled("'" + this.insertLiterals($compiled) + "';");
3160 * Represents a compiled template
3162 * @param compiled compiled template
3164 WCF.Template.Compiled = Class.extend({
3173 * Initializes our compiled template
3175 * @param $compiled compiled template
3177 init: function($compiled) {
3178 this._compiled = $compiled;
3182 * @see WCF.Template.fetch
3184 fetch: function($v) {
3185 return eval(this._compiled);
3192 * @param string element
3193 * @param array showItems
3194 * @param array hideItems
3196 WCF.ToggleOptions = Class.extend({
3205 * list of items to be shown
3212 * list of items to be hidden
3219 * Initializes option toggle.
3221 * @param string element
3222 * @param array showItems
3223 * @param array hideItems
3225 init: function(element, showItems, hideItems) {
3226 this._element = $('#' + element);
3227 this._showItems = showItems;
3228 this._hideItems = hideItems;
3231 this._element.click($.proxy(this._toggle, this));
3233 // execute toggle on init
3240 _toggle: function() {
3241 if (!this._element.attr('checked')) return;
3243 for (var $i = 0, $length = this._showItems.length; $i < $length; $i++) {
3244 var $item = this._showItems[$i];
3246 $('#' + $item).show();
3249 for (var $i = 0, $length = this._hideItems.length; $i < $length; $i++) {
3250 var $item = this._hideItems[$i];
3252 $('#' + $item).hide();
3258 * Namespace for all kind of collapsible containers.
3260 WCF.Collapsible = {};
3263 * Simple implementation for collapsible content, neither does it
3264 * store its state nor does it allow AJAX callbacks to fetch content.
3266 WCF.Collapsible.Simple = {
3268 * Initializes collapsibles.
3271 $('.jsCollapsible').each($.proxy(function(index, button) {
3272 this._initButton(button);
3277 * Binds an event listener on all buttons triggering the collapsible.
3279 * @param object button
3281 _initButton: function(button) {
3282 var $button = $(button);
3283 var $isOpen = $button.data('isOpen');
3286 // hide container on init
3287 $('#' + $button.data('collapsibleContainer')).hide();
3290 $button.click($.proxy(this._toggle, this));
3294 * Toggles collapsible containers on click.
3296 * @param object event
3298 _toggle: function(event) {
3299 var $button = $(event.currentTarget);
3300 var $isOpen = $button.data('isOpen');
3301 var $target = $('#' + $.wcfEscapeID($button.data('collapsibleContainer')));
3304 $target.stop().wcfBlindOut('vertical', $.proxy(function() {
3305 this._toggleImage($button, 'wcf.icon.closed');
3310 $target.stop().wcfBlindIn('vertical', $.proxy(function() {
3311 this._toggleImage($button, 'wcf.icon.opened');
3316 $button.data('isOpen', $isOpen);
3319 event.stopPropagation();
3324 * Toggles image of target button.
3326 * @param jQuery button
3327 * @param string image
3329 _toggleImage: function(button, image) {
3330 var $icon = WCF.Icon.get(image);
3331 var $image = button.find('img');
3333 if ($image.length) {
3334 $image.attr('src', $icon);
3340 * Basic implementation for collapsible containers with AJAX support. Results for open
3341 * and closed state will be cached.
3343 * @param string className
3345 WCF.Collapsible.Remote = Class.extend({
3353 * list of active containers
3359 * container meta data
3366 * @var WCF.Action.Proxy
3371 * Initializes the controller for collapsible containers with AJAX support.
3373 * @param string className
3375 init: function(className) {
3376 this._className = className;
3378 // validate containers
3379 var $containers = this._getContainers();
3380 if ($containers.length == 0) {
3381 console.debug('[WCF.Collapsible.Remote] Empty container set given, aborting.');
3384 this._proxy = new WCF.Action.Proxy({
3385 success: $.proxy(this._success, this)
3388 // initialize each container
3389 $containers.each($.proxy(function(index, container) {
3390 var $container = $(container);
3391 var $containerID = $container.wcfIdentify();
3392 this._containers[$containerID] = $container;
3394 this._initContainer($containerID);
3399 * Initializes a collapsible container.
3401 * @param string containerID
3403 _initContainer: function(containerID) {
3404 var $target = this._getTarget(containerID);
3405 var $buttonContainer = this._getButtonContainer(containerID);
3406 var $button = this._createButton(containerID, $buttonContainer);
3408 // store container meta data
3409 this._containerData[containerID] = {
3411 buttonContainer: $buttonContainer,
3412 isOpen: this._containers[containerID].data('isOpen'),
3418 * Returns a collection of collapsible containers.
3422 _getContainers: function() { },
3425 * Returns the target element for current collapsible container.
3427 * @param integer containerID
3430 _getTarget: function(containerID) { },
3433 * Returns the button container for current collapsible container.
3435 * @param integer containerID
3438 _getButtonContainer: function(containerID) { },
3441 * Creates the toggle button.
3443 * @param integer containerID
3444 * @param jQuery buttonContainer
3446 _createButton: function(containerID, buttonContainer) {
3447 var $isOpen = this._containers[containerID].data('isOpen');
3448 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);
3449 $button.data('containerID', containerID).click($.proxy(this._toggleContainer, this));
3455 * Toggles a container.
3457 * @param object event
3459 _toggleContainer: function(event) {
3460 var $button = $(event.currentTarget);
3461 var $containerID = $button.data('containerID');
3462 var $isOpen = this._containerData[$containerID].isOpen;
3463 var $state = ($isOpen) ? 'open' : 'close';
3464 var $newState = ($isOpen) ? 'close' : 'open';
3466 // fetch content state via AJAX
3467 this._proxy.setOption('data', {
3468 actionName: 'loadContainer',
3469 className: this._className,
3470 objectIDs: [ this._getObjectID($containerID) ],
3471 parameters: $.extend(true, {
3472 containerID: $containerID,
3473 currentState: $state,
3475 }, this._getAdditionalParameters($containerID))
3477 this._proxy.sendRequest();
3479 // set spinner for current button
3480 this._exchangeIcon($button);
3484 * Exchanges button icon.
3486 * @param jQuery button
3487 * @param string newIcon
3489 _exchangeIcon: function(button, newIcon) {
3490 newIcon = newIcon || WCF.Icon.get('wcf.icon.loading');
3491 button.find('img').attr('src', newIcon);
3495 * Returns the object id for current container.
3497 * @param integer containerID
3500 _getObjectID: function(containerID) {
3501 return $('#' + containerID).data('objectID');
3505 * Returns additional parameters.
3507 * @param integer containerID
3510 _getAdditionalParameters: function(containerID) {
3515 * Updates container content.
3517 * @param integer containerID
3518 * @param string newContent
3519 * @param string newState
3521 _updateContent: function(containerID, newContent, newState) {
3522 this._containerData[containerID].target.html(newContent);
3526 * Sets content upon successfull AJAX request.
3528 * @param object data
3529 * @param string textStatus
3530 * @param jQuery jqXHR
3532 _success: function(data, textStatus, jqXHR) {
3533 // validate container id
3534 if (!data.returnValues.containerID) return;
3535 var $containerID = data.returnValues.containerID;
3537 // check if container id is known
3538 if (!this._containers[$containerID]) return;
3540 // update content storage
3541 this._containerData[$containerID].isOpen = (data.returnValues.isOpen) ? true : false;
3542 var $newState = (data.returnValues.isOpen) ? 'open' : 'close';
3544 // update container content
3545 this._updateContent($containerID, data.returnValues.content, $newState);
3548 this._exchangeIcon(this._containerData[$containerID].button, WCF.Icon.get('wcf.icon.' + (data.returnValues.isOpen ? 'opened' : 'closed')));
3553 * Basic implementation for collapsible containers with AJAX support. Requires collapsible
3554 * content to be available in DOM already, if you want to load content on the fly use
3555 * WCF.Collapsible.Remote instead.
3557 WCF.Collapsible.SimpleRemote = WCF.Collapsible.Remote.extend({
3559 * Initializes an AJAX-based collapsible handler.
3561 * @param string className
3563 init: function(className) {
3564 this._super(className);
3566 // override settings for action proxy
3567 this._proxy = new WCF.Action.Proxy({
3568 showLoadingOverlay: false
3573 * @see WCF.Collapsible.Remote._initContainer()
3575 _initContainer: function(containerID) {
3576 this._super(containerID);
3578 // hide container on init if applicable
3579 if (!this._containerData[containerID].isOpen) {
3580 this._containerData[containerID].target.hide();
3581 this._exchangeIcon(this._containerData[containerID].button, WCF.Icon.get('wcf.icon.closed'));
3586 * Toggles container visibility.
3588 * @param object event
3590 _toggleContainer: function(event) {
3591 var $button = $(event.currentTarget);
3592 var $containerID = $button.data('containerID');
3593 var $isOpen = this._containerData[$containerID].isOpen;
3594 var $currentState = ($isOpen) ? 'open' : 'close';
3595 var $newState = ($isOpen) ? 'close' : 'open';
3597 this._proxy.setOption('data', {
3598 actionName: 'toggleContainer',
3599 className: this._className,
3600 objectIDs: [ this._getObjectID($containerID) ],
3601 parameters: $.extend(true, {
3602 containerID: $containerID,
3603 currentState: $currentState,
3605 }, this._getAdditionalParameters($containerID))
3607 this._proxy.sendRequest();
3610 this._exchangeIcon(this._containerData[$containerID].button, WCF.Icon.get('wcf.icon.' + ($newState === 'open' ? 'opened' : 'closed')));
3613 if ($newState === 'open') {
3614 this._containerData[$containerID].target.show();
3617 this._containerData[$containerID].target.hide();
3620 // update container data
3621 this._containerData[$containerID].isOpen = ($newState === 'open' ? true : false);
3626 * Provides collapsible sidebars with persistency support.
3628 WCF.Collapsible.Sidebar = Class.extend({
3630 * trigger button object
3636 * trigger button height
3648 * main container object
3651 _mainContainer: null,
3655 * @var WCF.Action.Proxy
3672 * sidebar identifier
3678 * sidebar offset from document top
3687 _userPanelHeight: 0,
3690 * Creates a new WCF.Collapsible.Sidebar object.
3693 this._sidebar = $('.sidebar:eq(0)');
3694 if (!this._sidebar.length) {
3695 console.debug("[WCF
.Collapsible
.Sidebar
] Could not find sidebar
, aborting
.");
3699 this._isOpen = (this._sidebar.data('isOpen')) ? true : false;
3700 this._sidebarName = this._sidebar.data('sidebarName');
3701 this._mainContainer = $('#main');
3702 this._sidebarHeight = this._sidebar.height();
3703 this._sidebarOffset = this._sidebar.getOffsets('offset').top;
3704 this._userPanelHeight = $('#topMenu').outerHeight();
3706 // add toggle button
3707 WCF.DOMNodeInsertedHandler.enable();
3708 this._button = $('<a class="collapsibleButton jsTooltip
" title="' + WCF.Language.get('wcf
.global
.button
.collapsible
') + '" />').prependTo(this._sidebar);
3709 this._button.click($.proxy(this._click, this));
3710 this._buttonHeight = this._button.outerHeight();
3711 WCF.DOMNodeInsertedHandler.disable();
3713 this._proxy = new WCF.Action.Proxy({
3714 showLoadingOverlay: false,
3715 url: 'index.php/AJAXInvoke/?t=' + SECURITY_TOKEN + SID_ARG_2ND
3718 $(document).scroll($.proxy(this._scroll, this)).resize($.proxy(this._scroll, this));
3720 this._renderSidebar();
3725 * Handles clicks on the trigger button.
3727 _click: function() {
3728 this._isOpen = (this._isOpen) ? false : true;
3730 this._proxy.setOption('data', {
3731 actionName: 'toggle',
3732 className: 'wcf\\system\\user\\collapsible\\content\\UserCollapsibleSidebarHandler',
3733 isOpen: (this._isOpen ? 1 : 0),
3734 sidebarName: this._sidebarName
3736 this._proxy.sendRequest();
3738 this._renderSidebar();
3742 * Aligns the toggle button upon scroll or resize.
3744 _scroll: function() {
3745 var $window = $(window);
3746 var $scrollOffset = $window.scrollTop();
3748 // calculate top and bottom coordinates of visible sidebar
3749 var $topOffset = Math.max($scrollOffset - this._sidebarOffset, 0);
3750 var $bottomOffset = Math.min(this._mainContainer.height(), ($window.height() + $scrollOffset) - this._sidebarOffset);
3753 if ($bottomOffset === $topOffset) {
3754 // sidebar not within visible area
3755 $buttonTop = this._sidebarOffset + this._sidebarHeight;
3758 $buttonTop = $topOffset + (($bottomOffset - $topOffset) / 2);
3760 // if the user panel is above the sidebar, substract it's height
3761 var $overlap = Math.max(Math.min($topOffset - this._userPanelHeight, this._userPanelHeight), 0);
3763 $buttonTop += ($overlap / 2);
3767 // ensure the button does not exceed bottom boundaries
3768 if (($bottomOffset - $topOffset - this._userPanelHeight) < this._buttonHeight) {
3769 $buttonTop = $buttonTop - this._buttonHeight;
3772 // exclude half button height
3773 $buttonTop = Math.max($buttonTop - (this._buttonHeight / 2), 0);
3776 this._button.css({ top: $buttonTop + 'px' });
3781 * Renders the sidebar state.
3783 _renderSidebar: function() {
3785 this._mainContainer.removeClass('sidebarCollapsed');
3788 this._mainContainer.addClass('sidebarCollapsed');
3791 // update button position
3797 * Holds userdata of the current user
3801 * id of the active user
3807 * name of the active user
3813 * Initializes userdata
3815 * @param integer userID
3816 * @param string username
3818 init: function(userID, username) {
3819 this.userID = userID;
3820 this.username = username;
3825 * Namespace for effect-related functions.
3830 * Scrolls to a specific element offset, optionally handling menu height.
3832 WCF.Effect.Scroll = Class.extend({
3834 * Scrolls to a specific element offset.
3836 * @param jQuery element
3837 * @param boolean excludeMenuHeight
3840 scrollTo: function(element, excludeMenuHeight) {
3841 if (!element.length) {
3845 var $elementOffset = element.getOffsets().top;
3846 var $documentHeight = $(document).height();
3847 var $windowHeight = $(window).height();
3849 // handles menu height
3850 if (excludeMenuHeight) {
3851 $elementOffset = Math.max($elementOffset - $('#topMenu').outerHeight(), 0);
3854 if ($elementOffset > $documentHeight - $windowHeight) {
3855 $elementOffset = $documentHeight - $windowHeight;
3856 if ($elementOffset < 0) {
3861 $('html,body').animate({ scrollTop: $elementOffset }, 400, function (x, t, b, c, d) {
3862 return -c * ( ( t = t / d - 1 ) * t * t * t - 1) + b;
3870 * Creates a smooth scroll effect.
3872 WCF.Effect.SmoothScroll = WCF.Effect.Scroll.extend({
3874 * Initializes effect.
3878 $(document).on('click', 'a[href$=#top],a[href$=#bottom]', function() {
3879 var $target = $(this.hash);
3880 self.scrollTo($target, true);
3888 * Creates the balloon tool-tip.
3890 WCF.Effect.BalloonTooltip = Class.extend({
3892 * initialization state
3904 * cache viewport dimensions
3907 _viewportDimensions: { },
3910 * Initializes tooltips.
3913 if (!this._didInit) {
3915 this._tooltip = $('<div id="balloonTooltip
" class="balloonTooltip
"><span id="balloonTooltipText
"></span><span class="pointer
"><span></span></span></div>').appendTo($('body')).hide();
3917 // get viewport dimensions
3918 this._updateViewportDimensions();
3920 // update viewport dimensions on resize
3921 $(window).resize($.proxy(this._updateViewportDimensions, this));
3923 // observe DOM changes
3924 WCF.DOMNodeInsertedHandler.addCallback('WCF.Effect.BalloonTooltip', $.proxy(this.init, this));
3926 this._didInit = true;
3930 $('.jsTooltip').each($.proxy(this._initTooltip, this));
3934 * Updates cached viewport dimensions.
3936 _updateViewportDimensions: function() {
3937 this._viewportDimensions = $(document).getDimensions();
3941 * Initializes a tooltip element.
3943 * @param integer index
3944 * @param object element
3946 _initTooltip: function(index, element) {
3947 var $element = $(element);
3949 if ($element.hasClass('jsTooltip')) {
3950 $element.removeClass('jsTooltip');
3951 var $title = $element.attr('title');
3953 // ignore empty elements
3954 if ($title !== '') {
3955 $element.data('tooltip', $title);
3956 $element.removeAttr('title');
3959 $.proxy(this._mouseEnterHandler, this),
3960 $.proxy(this._mouseLeaveHandler, this)
3962 $element.click($.proxy(this._mouseLeaveHandler, this));
3968 * Shows tooltip on hover.
3970 * @param object event
3972 _mouseEnterHandler: function(event) {
3973 var $element = $(event.currentTarget);
3975 var $title = $element.attr('title');
3976 if ($title && $title !== '') {
3977 $element.data('tooltip', $title);
3978 $element.removeAttr('title');
3981 // reset tooltip position
3987 // empty tooltip, skip
3988 if (!$element.data('tooltip')) {
3989 this._tooltip.hide();
3994 this._tooltip.children('span:eq(0)').text($element.data('tooltip'));
3997 var $arrow = this._tooltip.find('.pointer');
4000 this._tooltip.show();
4001 var $arrowWidth = $arrow.outerWidth();
4002 this._tooltip.hide();
4004 // calculate position
4005 var $elementOffsets = $element.getOffsets('offset');
4006 var $elementDimensions = $element.getDimensions('outer');
4007 var $tooltipDimensions = this._tooltip.getDimensions('outer');
4008 var $tooltipDimensionsInner = this._tooltip.getDimensions('inner');
4010 var $elementCenter = $elementOffsets.left + Math.ceil($elementDimensions.width / 2);
4011 var $tooltipHalfWidth = Math.ceil($tooltipDimensions.width / 2);
4013 // determine alignment
4014 var $alignment = 'center';
4015 if (($elementCenter - $tooltipHalfWidth) < 5) {
4016 $alignment = 'left';
4018 else if ((this._viewportDimensions.width - 5) < ($elementCenter + $tooltipHalfWidth)) {
4019 $alignment = 'right';
4022 // calculate top offset
4023 var $top = $elementOffsets.top + $elementDimensions.height + 7;
4025 // calculate left offset
4026 switch ($alignment) {
4028 var $left = Math.round($elementOffsets.left - $tooltipHalfWidth + ($elementDimensions.width / 2));
4031 left: ($tooltipDimensionsInner.width / 2 - $arrowWidth / 2) + "px
"
4036 var $left = $elementOffsets.left;
4044 var $left = $elementOffsets.left + $elementDimensions.width - $tooltipDimensions.width;
4047 left: ($tooltipDimensionsInner.width - $arrowWidth - 5) + "px
"
4059 this._tooltip.wcfFadeIn();
4063 * Hides tooltip once cursor left the element.
4065 * @param object event
4067 _mouseLeaveHandler: function(event) {
4068 this._tooltip.stop().hide().css({
4075 * Handles clicks outside an overlay, hitting body-tag through bubbling.
4077 * You should always remove callbacks before disposing the attached element,
4078 * preventing errors from blocking the iteration. Furthermore you should
4079 * always handle clicks on your overlay's container and return 'false' to
4082 WCF.CloseOverlayHandler = {
4085 * @var WCF.Dictionary
4087 _callbacks: new WCF.Dictionary(),
4090 * indicates that overlay handler is listening to click events on body-tag
4093 _isListening: false,
4096 * Adds a new callback.
4098 * @param string identifier
4099 * @param object callback
4101 addCallback: function(identifier, callback) {
4102 this._bindListener();
4104 if (this._callbacks.isset(identifier)) {
4105 console.debug("[WCF
.CloseOverlayHandler
] identifier
'" + identifier + "' is already bound to a callback
");
4109 this._callbacks.add(identifier, callback);
4113 * Removes a callback from list.
4115 * @param string identifier
4117 removeCallback: function(identifier) {
4118 if (this._callbacks.isset(identifier)) {
4119 this._callbacks.remove(identifier);
4124 * Binds click event handler.
4126 _bindListener: function() {
4127 if (this._isListening) return;
4129 $('body').click($.proxy(this._executeCallbacks, this));
4131 this._isListening = true;
4135 * Executes callbacks on click.
4137 _executeCallbacks: function(event) {
4138 this._callbacks.each(function(pair) {
4146 * Notifies objects once a DOM node was inserted.
4148 WCF.DOMNodeInsertedHandler = {
4151 * @var WCF.Dictionary
4153 _callbacks: new WCF.Dictionary(),
4156 * true if DOMNodeInserted event should be ignored
4159 _discardEvent: true,
4162 * counts requests to enable WCF.DOMNodeInsertedHandler
4165 _discardEventCount: 0,
4168 * prevent infinite loop if a callback manipulates DOM
4171 _isExecuting: false,
4174 * indicates that overlay handler is listening to click events on body-tag
4177 _isListening: false,
4180 * Adds a new callback.
4182 * @param string identifier
4183 * @param object callback
4185 addCallback: function(identifier, callback) {
4186 this._discardEventCount = 0;
4187 this._bindListener();
4189 if (this._callbacks.isset(identifier)) {
4190 console.debug("[WCF
.DOMNodeInsertedHandler
] identifier
'" + identifier + "' is already bound to a callback
");
4194 this._callbacks.add(identifier, callback);
4198 * Removes a callback from list.
4200 * @param string identifier
4202 removeCallback: function(identifier) {
4203 if (this._callbacks.isset(identifier)) {
4204 this._callbacks.remove(identifier);
4209 * Binds click event handler.
4211 _bindListener: function() {
4212 if (this._isListening) return;
4214 $(document).bind('DOMNodeInserted', $.proxy(this._executeCallbacks, this));
4216 this._isListening = true;
4220 * Executes callbacks on click.
4222 _executeCallbacks: function() {
4223 if (this._discardEvent || this._isExecuting) return;
4225 // do not track events while executing callbacks
4226 this._isExecuting = true;
4228 this._callbacks.each(function(pair) {
4233 // enable listener again
4234 this._isExecuting = false;
4238 * Disables DOMNodeInsertedHandler, should be used after you've enabled it.
4240 disable: function() {
4241 this._discardEventCount--;
4243 if (this._discardEventCount < 1) {
4244 this._discardEvent = true;
4245 this._discardEventCount = 0;
4250 * Enables DOMNodeInsertedHandler, should be used if you're inserting HTML (e.g. via AJAX)
4251 * which might contain event-related elements. You have to disable the DOMNodeInsertedHandler
4252 * once you've enabled it, if you fail it will cause an infinite loop!
4254 enable: function() {
4255 this._discardEventCount++;
4257 this._discardEvent = false;
4261 * Forces execution of DOMNodeInsertedHandler.
4263 forceExecution: function() {
4265 this._executeCallbacks();
4271 * Notifies objects once a DOM node was removed.
4273 WCF.DOMNodeRemovedHandler = {
4276 * @var WCF.Dictionary
4278 _callbacks: new WCF.Dictionary(),
4281 * prevent infinite loop if a callback manipulates DOM
4284 _isExecuting: false,
4287 * indicates that overlay handler is listening to DOMNodeRemoved events on body-tag
4290 _isListening: false,
4293 * Adds a new callback.
4295 * @param string identifier
4296 * @param object callback
4298 addCallback: function(identifier, callback) {
4299 this._bindListener();
4301 if (this._callbacks.isset(identifier)) {
4302 console.debug("[WCF
.DOMNodeRemovedHandler
] identifier
'" + identifier + "' is already bound to a callback
");
4306 this._callbacks.add(identifier, callback);
4310 * Removes a callback from list.
4312 * @param string identifier
4314 removeCallback: function(identifier) {
4315 if (this._callbacks.isset(identifier)) {
4316 this._callbacks.remove(identifier);
4321 * Binds click event handler.
4323 _bindListener: function() {
4324 if (this._isListening) return;
4326 $(document).bind('DOMNodeRemoved', $.proxy(this._executeCallbacks, this));
4328 this._isListening = true;
4332 * Executes callbacks if a DOM node is removed.
4334 _executeCallbacks: function(event) {
4335 if (this._isExecuting) return;
4337 // do not track events while executing callbacks
4338 this._isExecuting = true;
4340 this._callbacks.each(function(pair) {
4345 // enable listener again
4346 this._isExecuting = false;
4351 * Namespace for table related classes.
4356 * Handles empty tables which can be used in combination with WCF.Action.Proxy.
4358 WCF.Table.EmptyTableHandler = Class.extend({
4366 * class name of the relevant rows
4372 * Initalizes a new WCF.Table.EmptyTableHandler object.
4374 * @param jQuery tableContainer
4375 * @param string rowClassName
4376 * @param object options
4378 init: function(tableContainer, rowClassName, options) {
4379 this._rowClassName = rowClassName;
4380 this._tableContainer = tableContainer;
4382 this._options = $.extend(true, {
4384 messageType: 'info',
4386 updatePageNumber: false
4389 WCF.DOMNodeRemovedHandler.addCallback('WCF.Table.EmptyTableHandler.' + rowClassName, $.proxy(this._remove, this));
4393 * Handles the removal of a DOM node.
4395 _remove: function(event) {
4396 var element = $(event.target);
4398 // check if DOM element is relevant
4399 if (element.hasClass(this._rowClassName)) {
4400 var tbody = element.parents('tbody:eq(0)');
4402 // check if table will be empty if DOM node is removed
4403 if (tbody.children('tr').length == 1) {
4404 if (this._options.emptyMessage) {
4406 this._tableContainer.replaceWith($('<p />').addClass(this._options.messageType).text(this._options.emptyMessage));
4408 else if (this._options.refreshPage) {
4410 if (this._options.updatePageNumber) {
4411 // calculate the new page number
4412 var pageNumberURLComponents = window.location.href.match(/(\?|&)pageNo=(\d+)/g);
4413 if (pageNumberURLComponents) {
4414 var currentPageNumber = pageNumberURLComponents[pageNumberURLComponents.length - 1].match(/\d+/g);
4415 if (this._options.updatePageNumber > 0) {
4416 currentPageNumber++;
4419 currentPageNumber--;
4422 window.location = window.location.href.replace(pageNumberURLComponents[pageNumberURLComponents.length - 1], pageNumberURLComponents[pageNumberURLComponents.length - 1][0] + 'pageNo=' + currentPageNumber);
4426 window.location.reload();
4430 // simply remove the table container
4431 this._tableContainer.remove();
4439 * Namespace for search related classes.
4444 * Performs a quick search.
4446 WCF.Search.Base = Class.extend({
4448 * notification callback
4460 * comma seperated list
4463 _commaSeperated: false,
4466 * list with values that are excluded from seaching
4469 _excludedSearchValues: [],
4472 * count of available results
4478 * item index, -1 if none is selected
4490 * old search string, used for comparison
4491 * @var array<string>
4493 _oldSearchString: [ ],
4497 * @var WCF.Action.Proxy
4502 * search input field
4508 * minimum search input length, MUST be 1 or higher
4514 * Initializes a new search.
4516 * @param jQuery searchInput
4517 * @param object callback
4518 * @param array excludedSearchValues
4519 * @param boolean commaSeperated
4520 * @param boolean showLoadingOverlay
4522 init: function(searchInput, callback, excludedSearchValues, commaSeperated, showLoadingOverlay) {
4523 if (callback !== null && callback !== undefined && !$.isFunction(callback)) {
4524 console.debug("[WCF
.Search
.Base
] The given callback is invalid
, aborting
.");
4528 this._callback = (callback) ? callback : null;
4529 this._excludedSearchValues = [];
4530 if (excludedSearchValues) {
4531 this._excludedSearchValues = excludedSearchValues;
4534 this._searchInput = $(searchInput);
4535 if (!this._searchInput.length) {
4536 console.debug("[WCF
.Search
.Base
] Selector
'" + searchInput + "' for search input is invalid
, aborting
.");
4540 this._searchInput.keydown($.proxy(this._keyDown, this)).keyup($.proxy(this._keyUp, this)).wrap('<span class="dropdown
" />');
4541 this._list = $('<ul class="dropdownMenu
" />').insertAfter(this._searchInput);
4542 this._commaSeperated = (commaSeperated) ? true : false;
4543 this._oldSearchString = [ ];
4545 this._itemCount = 0;
4546 this._itemIndex = -1;
4548 this._proxy = new WCF.Action.Proxy({
4549 showLoadingOverlay: (showLoadingOverlay === false ? false : true),
4550 success: $.proxy(this._success, this)
4553 if (this._searchInput.getTagName() === 'input') {
4554 this._searchInput.attr('autocomplete', 'off');
4557 this._searchInput.blur($.proxy(this._blur, this));
4561 * Closes the dropdown after a short delay.
4565 new WCF.PeriodicalExecuter(function(pe) {
4566 if (self._list.is(':visible')) {
4567 self._clearList(false);
4575 * Blocks execution of 'Enter' event.
4577 * @param object event
4579 _keyDown: function(event) {
4580 if (event.which === 13) {
4581 event.preventDefault();
4586 * Performs a search upon key up.
4588 * @param object event
4590 _keyUp: function(event) {
4591 // handle arrow keys and return key
4592 switch (event.which) {
4593 case 37: // arrow-left
4594 case 39: // arrow-right
4598 case 38: // arrow up
4599 this._selectPreviousItem();
4603 case 40: // arrow down
4604 this._selectNextItem();
4608 case 13: // return key
4609 return this._selectElement(event);
4613 var $content = this._getSearchString(event);
4614 if ($content === '') {
4615 this._clearList(true);
4617 else if ($content.length >= this._triggerLength) {
4620 excludedSearchValues: this._excludedSearchValues,
4621 searchString: $content
4625 this._proxy.setOption('data', {
4626 actionName: 'getSearchResultList',
4627 className: this._className,
4628 parameters: this._getParameters($parameters)
4630 this._proxy.sendRequest();
4633 // input below trigger length
4634 this._clearList(false);
4639 * Selects the next item in list.
4641 _selectNextItem: function() {
4642 if (this._itemCount === 0) {
4646 // remove previous marking
4648 if (this._itemIndex === this._itemCount) {
4649 this._itemIndex = 0;
4652 this._highlightSelectedElement();
4656 * Selects the previous item in list.
4658 _selectPreviousItem: function() {
4659 if (this._itemCount === 0) {
4664 if (this._itemIndex === -1) {
4665 this._itemIndex = this._itemCount - 1;
4668 this._highlightSelectedElement();
4672 * Highlights the active item.
4674 _highlightSelectedElement: function() {
4675 this._list.find('li').removeClass('dropdownNavigationItem');
4676 this._list.find('li:eq(' + this._itemIndex + ')').addClass('dropdownNavigationItem');
4680 * Selects the active item by pressing the return key.
4682 * @param object event
4685 _selectElement: function(event) {
4686 if (this._itemCount === 0) {
4690 this._list.find('li.dropdownNavigationItem').trigger('click');
4696 * Returns search string.
4700 _getSearchString: function(event) {
4701 var $searchString = $.trim(this._searchInput.val());
4702 if (this._commaSeperated) {
4703 var $keyCode = event.keyCode || event.which;
4704 if ($keyCode == 188) {
4705 // ignore event if char is 188 = ,
4709 var $current = $searchString.split(',');
4710 var $length = $current.length;
4711 for (var $i = 0; $i < $length; $i++) {
4712 // remove whitespaces at the beginning or end
4713 $current[$i] = $.trim($current[$i]);
4716 for (var $i = 0; $i < $length; $i++) {
4717 var $part = $current[$i];
4719 if (this._oldSearchString[$i]) {
4721 if ($part != this._oldSearchString[$i]) {
4722 // current part was changed
4723 $searchString = $part;
4728 // new part was added
4729 $searchString = $part;
4734 this._oldSearchString = $current;
4737 return $searchString;
4741 * Returns parameters for quick search.
4743 * @param object parameters
4746 _getParameters: function(parameters) {
4751 * Evalutes search results.
4753 * @param object data
4754 * @param string textStatus
4755 * @param jQuery jqXHR
4757 _success: function(data, textStatus, jqXHR) {
4758 this._clearList(false);
4760 // no items available, abort
4761 if (!$.getLength(data.returnValues)) {
4765 for (var $i in data.returnValues) {
4766 var $item = data.returnValues[$i];
4768 this._createListItem($item);
4771 this._list.parent().addClass('dropdownOpen');
4772 WCF.Dropdown.setAlignment(undefined, this._list);
4774 WCF.CloseOverlayHandler.addCallback('WCF.Search.Base', $.proxy(function() { this._clearList(true); }, this));
4778 * Creates a new list item.
4780 * @param object item
4783 _createListItem: function(item) {
4784 var $listItem = $('<li><span>' + item.label + '</span></li>').appendTo(this._list);
4785 $listItem.data('objectID', item.objectID).data('label', item.label).click($.proxy(this._executeCallback, this));
4793 * Executes callback upon result click.
4795 * @param object event
4797 _executeCallback: function(event) {
4798 var $clearSearchInput = false;
4799 var $listItem = $(event.currentTarget);
4801 if (this._commaSeperated) {
4802 // auto-complete current part
4803 var $result = $listItem.data('label');
4804 for (var $i = 0, $length = this._oldSearchString.length; $i < $length; $i++) {
4805 var $part = this._oldSearchString[$i];
4806 if ($result.toLowerCase().indexOf($part.toLowerCase()) === 0) {
4807 this._oldSearchString[$i] = $result;
4808 this._searchInput.attr('value', this._oldSearchString.join(', '));
4810 if ($.browser.webkit) {
4811 // chrome won't display the new value until the textarea is rendered again
4812 // this quick fix forces chrome to render it again, even though it changes nothing
4813 this._searchInput.css({ display: 'block' });
4816 // set focus on input field again
4817 var $position = this._searchInput.val().toLowerCase().indexOf($result.toLowerCase()) + $result.length;
4818 this._searchInput.focus().setCaret($position);
4825 if (this._callback === null) {
4826 this._searchInput.val($listItem.data('label'));
4829 $clearSearchInput = (this._callback($listItem.data()) === true) ? true : false;
4833 // close list and revert input
4834 this._clearList($clearSearchInput);
4838 * Closes the suggestion list and clears search input on demand.
4840 * @param boolean clearSearchInput
4842 _clearList: function(clearSearchInput) {
4843 if (clearSearchInput && !this._commaSeperated) {
4844 this._searchInput.val('');
4847 this._list.parent().removeClass('dropdownOpen').end().empty();
4849 WCF.CloseOverlayHandler.removeCallback('WCF.Search.Base');
4851 // reset item navigation
4852 this._itemCount = 0;
4853 this._itemIndex = -1;
4857 * Adds an excluded search value.
4859 * @param string value
4861 addExcludedSearchValue: function(value) {
4862 if (!WCF.inArray(value, this._excludedSearchValues)) {
4863 this._excludedSearchValues.push(value);
4868 * Adds an excluded search value.
4870 * @param string value
4872 removeExcludedSearchValue: function(value) {
4873 var index = $.inArray(value, this._excludedSearchValues);
4875 this._excludedSearchValues.splice(index, 1);
4881 * Provides quick search for users and user groups.
4883 * @see WCF.Search.Base
4885 WCF.Search.User = WCF.Search.Base.extend({
4887 * @see WCF.Search.Base._className
4889 _className: 'wcf\\data\\user\\UserAction',
4892 * include user groups in search
4895 _includeUserGroups: false,
4898 * @see WCF.Search.Base.init()
4900 init: function(searchInput, callback, includeUserGroups, excludedSearchValues, commaSeperated) {
4901 this._includeUserGroups = includeUserGroups;
4903 this._super(searchInput, callback, excludedSearchValues, commaSeperated);
4907 * @see WCF.Search.Base._getParameters()
4909 _getParameters: function(parameters) {
4910 parameters.data.includeUserGroups = this._includeUserGroups ? 1 : 0;
4916 * @see WCF.Search.Base._createListItem()
4918 _createListItem: function(item) {
4919 var $listItem = this._super(item);
4922 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)'));
4923 $listItem.data('type', item.type);
4930 * Namespace for system-related classes.
4935 * System notification overlays.
4937 * @param string message
4938 * @param string cssClassNames
4940 WCF.System.Notification = Class.extend({
4942 * callback on notification close
4954 * notification message
4960 * notification overlay
4966 * Creates a new system notification overlay.
4968 * @param string message
4969 * @param string cssClassNames
4971 init: function(message, cssClassNames) {
4972 this._cssClassNames = cssClassNames || 'success';
4973 this._message = message;
4974 this._overlay = $('#systemNotification');
4976 if (!this._overlay.length) {
4977 this._overlay = $('<div id="systemNotification
"><p></p></div>').appendTo(document.body);
4982 * Shows the notification overlay.
4984 * @param object callback
4985 * @param integer duration
4986 * @param string message
4987 * @param string cssClassName
4989 show: function(callback, duration, message, cssClassNames) {
4990 duration = parseInt(duration);
4991 if (!duration) duration = 2000;
4993 if (callback && $.isFunction(callback)) {
4994 this._callback = callback;
4997 this._overlay.children('p').html((message || this._message));
4998 this._overlay.children('p').removeClass().addClass((cssClassNames || this._cssClassNames));
5000 // hide overlay after specified duration
5001 new WCF.PeriodicalExecuter($.proxy(this._hide, this), duration);
5003 this._overlay.addClass('open');
5007 * Hides the notification overlay after executing the callback.
5009 * @param WCF.PeriodicalExecuter pe
5011 _hide: function(pe) {
5012 if (this._callback !== null) {
5016 this._overlay.removeClass('open');
5023 * Provides dialog-based confirmations.
5025 WCF.System.Confirmation = {
5027 * notification callback
5033 * confirmation dialog
5039 * callback parameters
5051 * Displays a confirmation dialog.
5053 * @param string message
5054 * @param object callback
5055 * @param object parameters
5056 * @param jQuery template
5058 show: function(message, callback, parameters, template) {
5059 if (this._visible) {
5060 console.debug('[WCF.System.Confirmation] Confirmation dialog is already open, refusing action.');
5064 if (!$.isFunction(callback)) {
5065 console.debug('[WCF.System.Confirmation] Given callback is invalid, aborting.');
5069 this._callback = callback;
5070 this._parameters = parameters;
5073 if (this._dialog === null) {
5074 this._createDialog();
5078 this._dialog.find('#wcfSystemConfirmationContent').empty().hide();
5079 if (template && template.length) {
5080 template.appendTo(this._dialog.find('#wcfSystemConfirmationContent').show());
5083 this._dialog.find('p').html(message);
5084 this._dialog.wcfDialog({
5085 onClose: $.proxy(this._close, this),
5086 onShow: $.proxy(this._show, this),
5087 title: WCF.Language.get('wcf.global.confirmation.title')
5090 this._dialog.wcfDialog('render');
5093 this._visible = true;
5097 * Creates the confirmation dialog on first use.
5099 _createDialog: function() {
5100 this._dialog = $('<div id="wcfSystemConfirmation
" class="systemConfirmation
"><p /><div id="wcfSystemConfirmationContent
" /></div>').hide().appendTo(document.body);
5101 var $formButtons = $('<div class="formSubmit
" />').appendTo(this._dialog);
5103 $('<button class="buttonPrimary
">' + WCF.Language.get('wcf.global.confirmation.confirm') + '</button>').data('action', 'confirm').click($.proxy(this._click, this)).appendTo($formButtons);
5104 $('<button>' + WCF.Language.get('wcf.global.confirmation.cancel') + '</button>').data('action', 'cancel').click($.proxy(this._click, this)).appendTo($formButtons);
5108 * Handles button clicks.
5110 * @param object event
5112 _click: function(event) {
5113 this._notify($(event.currentTarget).data('action'));
5117 * Handles dialog being closed.
5119 _close: function() {
5120 if (this._visible) {
5121 this._notify('cancel');
5126 * Notifies callback upon user's decision.
5128 * @param string action
5130 _notify: function(action) {
5131 this._visible = false;
5132 this._dialog.wcfDialog('close');
5134 this._callback(action, this._parameters);
5138 * Tries to set focus on confirm button.
5141 this._dialog.find('button.buttonPrimary').blur().focus();
5146 * Provides the 'jump to page' overlay.
5148 WCF.System.PageNavigation = {
5156 * page No description
5174 * list of tracked navigation bars
5186 * Initializes the 'jump to page' overlay for given selector.
5188 * @param string selector
5189 * @param object callback
5191 init: function(selector, callback) {
5192 var $elements = $(selector);
5193 if (!$elements.length) {
5197 callback = callback || null;
5198 if (callback !== null && !$.isFunction(callback)) {
5199 console.debug("[WCF
.System
.PageNavigation
] Callback
for selector
'" + selector + "' is invalid
, aborting
.");
5203 this._initElements($elements, callback);
5207 * Initializes the 'jump to page' overlay for given elements.
5209 * @param jQuery elements
5210 * @param object callback
5212 _initElements: function(elements, callback) {
5214 elements.each(function(index, element) {
5215 var $element = $(element);
5216 console.debug($element.data());
5217 var $elementID = $element.wcfIdentify();
5218 if (self._elements[$elementID] === undefined) {
5219 self._elements[$elementID] = $element;
5220 $element.find('li.jumpTo').data('elementID', $elementID).click($.proxy(self._click, self));
5222 }).data('callback', callback);
5226 * Shows the 'jump to page' overlay.
5228 * @param object event
5230 _click: function(event) {
5231 this._elementID = $(event.currentTarget).data('elementID');
5233 if (this._dialog === null) {
5234 this._dialog = $('<div id="pageNavigationOverlay
" />').hide().appendTo(document.body);
5236 var $fieldset = $('<fieldset><legend>' + WCF.Language.get('wcf.global.page.jumpTo') + '</legend></fieldset>').appendTo(this._dialog);
5237 $('<dl><dt><label for="jsPageNavigationPageNo
">' + WCF.Language.get('wcf.global.page.jumpTo') + '</label></dt><dd></dd></dl>').appendTo($fieldset);
5238 this._pageNo = $('<input type="number
" id="jsPageNavigationPageNo
" value="1" min="1" max="1" class="long" />').keyup($.proxy(this._keyUp, this)).appendTo($fieldset.find('dd'));
5239 this._description = $('<small></small>').insertAfter(this._pageNo);
5240 var $formSubmit = $('<div class="formSubmit
" />').appendTo(this._dialog);
5241 this._button = $('<button class="buttonPrimary
">' + WCF.Language.get('wcf.global.button.submit') + '</button>').click($.proxy(this._submit, this)).appendTo($formSubmit);
5244 this._button.enable();
5245 this._description.html(WCF.Language.get('wcf.global.page.jumpTo.description').replace(/#pages#/, this._elements[this._elementID].data('pages')));
5246 this._pageNo.val('1').attr('max', this._elements[this._elementID].data('pages'));
5248 this._dialog.wcfDialog({
5249 'title': WCF.Language.get('wcf.global.page.pageNavigation')
5254 * Validates the page No input.
5256 _keyUp: function() {
5257 var $pageNo = parseInt(this._pageNo.val()) || 0;
5258 if ($pageNo < 1 || $pageNo > this._pageNo.attr('max')) {
5259 this._button.disable();
5262 this._button.enable();
5267 * Redirects to given page No.
5269 _submit: function() {
5270 var $pageNavigation = this._elements[this._elementID];
5271 if ($pageNavigation.data('callback') === null) {
5272 var $redirectURL = $pageNavigation.data('link').replace(/pageNo=%d/, 'pageNo=' + this._pageNo.val());
5273 window.location = $redirectURL;
5276 $pageNavigation.data('callback')(this._pageNo.val());
5277 this._dialog.wcfDialog('close');
5283 * Sends periodical requests to protect the session from expiring. By default
5284 * it will send a request 1 minute before it would expire.
5286 * @param integer seconds
5288 WCF.System.KeepAlive = Class.extend({
5290 * Initializes the WCF.System.KeepAlive class.
5292 * @param integer seconds
5294 init: function(seconds) {
5295 new WCF.PeriodicalExecuter(function() {
5296 new WCF.Action.Proxy({
5299 actionName: 'keepAlive',
5300 className: 'wcf\\data\\session\\SessionAction'
5302 showLoadingOverlay: false
5304 }, (seconds * 1000));
5309 * Default implementation for inline editors.
5311 * @param string elementSelector
5313 WCF.InlineEditor = Class.extend({
5315 * list of registered callbacks
5316 * @var array<object>
5321 * list of dropdown selections
5327 * list of container elements
5333 * notification object
5334 * @var WCF.System.Notification
5336 _notification: null,
5339 * list of known options
5340 * @var array<object>
5346 * @var WCF.Action.Proxy
5351 * list of data to update upon success
5352 * @var array<object>
5357 * Initializes a new inline editor.
5359 init: function(elementSelector) {
5360 var $elements = $(elementSelector);
5361 if (!$elements.length) {
5366 $elements.each(function(index, element) {
5367 var $element = $(element);
5368 var $elementID = $element.wcfIdentify();
5370 // find trigger element
5371 var $trigger = self._getTriggerElement($element);
5372 if ($trigger === null || $trigger.length !== 1) {
5376 $trigger.click($.proxy(self._show, self)).data('elementID', $elementID);
5379 self._elements[$elementID] = $element;
5382 this._proxy = new WCF.Action.Proxy({
5383 success: $.proxy(this._success, this)
5388 WCF.CloseOverlayHandler.addCallback('WCF.InlineEditor', $.proxy(this._closeAll, this));
5390 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success');
5394 * Closes all inline editors.
5396 _closeAll: function() {
5397 for (var $elementID in this._elements) {
5398 this._hide($elementID);
5403 * Sets options for this inline editor.
5405 _setOptions: function() {
5406 this._options = [ ];
5410 * Register an option callback for validation and execution.
5412 * @param object callback
5414 registerCallback: function(callback) {
5415 if ($.isFunction(callback)) {
5416 this._callbacks.push(callback);
5421 * Returns the triggering element.
5423 * @param jQuery element
5426 _getTriggerElement: function(element) {
5431 * Shows a dropdown menu if options are available.
5433 * @param object event
5435 _show: function(event) {
5436 var $elementID = $(event.currentTarget).data('elementID');
5439 if (!this._dropdowns[$elementID]) {
5440 var $trigger = this._getTriggerElement(this._elements[$elementID]).addClass('dropdownToggle').wrap('<span class="dropdown
" />');
5441 var $dropdown = $trigger.parent('span');
5442 $trigger.data('target', $dropdown.wcfIdentify());
5443 this._dropdowns[$elementID] = $('<ul class="dropdownMenu
" style="top
: ' + ($dropdown.outerHeight() + 14) + 'px
;" />').insertAfter($trigger);
5445 this._dropdowns[$elementID].empty();
5448 var $hasOptions = false;
5449 var $lastElementType = '';
5450 for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
5451 var $option = this._options[$i];
5453 if ($option.optionName === 'divider') {
5454 if ($lastElementType !== '' && $lastElementType !== 'divider') {
5455 $('<li class="dropdownDivider
" />').appendTo(this._dropdowns[$elementID]);
5456 $lastElementType = $option.optionName;
5459 else if (this._validate($elementID, $option.optionName) || this._validateCallbacks($elementID, $option.optionName)) {
5460 var $listItem = $('<li><span>' + $option.label + '</span></li>').appendTo(this._dropdowns[$elementID]);
5461 $listItem.data('elementID', $elementID).data('optionName', $option.optionName).click($.proxy(this._click, this));
5464 $lastElementType = $option.optionName;
5469 this._dropdowns[$elementID].parent('span').addClass('dropdownOpen');
5476 * Validates an option.
5478 * @param string elementID
5479 * @param string optionName
5482 _validate: function(elementID, optionName) {
5487 * Validates an option provided by callbacks.
5489 * @param string elementID
5490 * @param string optionName
5493 _validateCallbacks: function(elementID, optionName) {
5494 var $length = this._callbacks.length;
5496 for (var $i = 0; $i < $length; $i++) {
5497 if (this._callbacks[$i].validate(this._elements[elementID], optionName)) {
5507 * Handles AJAX responses.
5509 * @param object data
5510 * @param string textStatus
5511 * @param jQuery jqXHR
5513 _success: function(data, textStatus, jqXHR) {
5514 var $length = this._updateData.length;
5519 this._updateState();
5521 this._updateData = [ ];
5525 * Update element states based upon update data.
5527 _updateState: function() { },
5530 * Handles clicks within dropdown.
5532 * @param object event
5534 _click: function(event) {
5535 var $listItem = $(event.currentTarget);
5536 var $elementID = $listItem.data('elementID');
5537 var $optionName = $listItem.data('optionName');
5539 if (!this._execute($elementID, $optionName)) {
5540 this._executeCallback($elementID, $optionName);
5543 this._hide($elementID);
5547 * Executes actions associated with an option.
5549 * @param string elementID
5550 * @param string optionName
5553 _execute: function(elementID, optionName) {
5558 * Executes actions associated with an option provided by callbacks.
5560 * @param string elementID
5561 * @param string optionName
5564 _executeCallback: function(elementID, optionName) {
5565 var $length = this._callbacks.length;
5567 for (var $i = 0; $i < $length; $i++) {
5568 if (this._callbacks[$i].execute(this._elements[elementID], optionName)) {
5578 * Hides a dropdown menu.
5580 * @param string elementID
5582 _hide: function(elementID) {
5583 if (this._dropdowns[elementID]) {
5584 this._dropdowns[elementID].empty().parent('span').removeClass('dropdownOpen');
5590 * Default implementation for ajax file uploads
5592 * @param jquery buttonSelector
5593 * @param jquery fileListSelector
5594 * @param string className
5595 * @param jquery options
5597 WCF.Upload = Class.extend({
5599 * name of the upload field
5608 _buttonSelector: null,
5611 * file list selector
5614 _fileListSelector: null,
5629 * additional options
5641 * true, if the active user's browser supports ajax file uploads
5644 _supportsAJAXUpload: true,
5647 * fallback overlay for stupid browsers
5653 * Initializes a new upload handler.
5655 init: function(buttonSelector, fileListSelector, className, options) {
5656 this._buttonSelector = buttonSelector;
5657 this._fileListSelector = fileListSelector;
5658 this._className = className;
5659 this._options = $.extend(true, {
5662 url: 'index.php/AJAXUpload/?t=' + SECURITY_TOKEN + SID_ARG_2ND
5665 // check for ajax upload support
5666 var $xhr = new XMLHttpRequest();
5667 this._supportsAJAXUpload = ($xhr && ('upload' in $xhr) && ('onprogress' in $xhr.upload));
5669 // create upload button
5670 this._createButton();
5674 * Creates the upload button.
5676 _createButton: function() {
5677 if (this._supportsAJAXUpload) {
5678 this._fileUpload = $('<input type="file
" name="'+this._name+'" '+(this._options.multiple ? 'multiple="true" ' : '')+'/>');
5679 this._fileUpload.change($.proxy(this._upload, this));
5680 var $button = $('<p class="button uploadButton
"><span>'+WCF.Language.get('wcf.global.button.upload')+'</span></p>');
5681 $button.append(this._fileUpload);
5684 var $button = $('<p class="button
"><span>Upload</span></p>');
5685 $button.click($.proxy(this._showOverlay, this));
5688 this._insertButton($button);
5692 * Inserts the upload button.
5694 _insertButton: function(button) {
5695 this._buttonSelector.append(button);
5699 * Callback for file uploads.
5701 _upload: function() {
5702 var $files = this._fileUpload.prop('files');
5704 if ($files.length > 0) {
5705 var $fd = new FormData();
5707 var $uploadID = this._uploadMatrix.length;
5708 this._uploadMatrix[$uploadID] = [];
5710 for (var $i = 0; $i < $files.length; $i++) {
5711 var $li = this._initFile($files[$i]);
5712 $li.data('filename', $files[$i].name);
5713 this._uploadMatrix[$uploadID].push($li);
5714 $fd.append('__files[]', $files[$i]);
5716 $fd.append('actionName', this._options.action);
5717 $fd.append('className', this._className);
5718 var $additionalParameters = this._getParameters();
5719 for (var $name in $additionalParameters) {
5720 $fd.append('parameters['+$name+']', $additionalParameters[$name]);
5725 url: this._options.url,
5726 enctype: 'multipart/form-data',
5730 success: function(data, textStatus, jqXHR) {
5731 self._success($uploadID, data);
5733 error: $.proxy(this._error, this),
5735 var $xhr = $.ajaxSettings.xhr();
5737 $xhr.upload.addEventListener('progress', function(event) {
5738 self._progress($uploadID, event);
5748 * Callback for success event
5750 _success: function(uploadID, data) {
5751 console.debug(data);
5755 * Callback for error event
5757 _error: function(jqXHR, textStatus, errorThrown) {
5758 console.debug(jqXHR.responseText);
5762 * Callback for progress event
5764 _progress: function(uploadID, event) {
5765 var $percentComplete = Math.round(event.loaded * 100 / event.total);
5767 for (var $i = 0; $i < this._uploadMatrix[uploadID].length; $i++) {
5768 this._uploadMatrix[uploadID][$i].find('progress').attr('value', $percentComplete);
5773 * Returns additional parameters.
5775 _getParameters: function() {
5779 _initFile: function(file) {
5780 var $li = $('<li>'+file.name+' ('+file.size+')<progress max="100"></progress></li>');
5781 this._fileListSelector.append($li);
5787 * Shows the fallback overlay (work in progress)
5789 _showOverlay: function() {
5791 if (!this._overlay) {
5793 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>');
5797 var $iframe = $('<iframe style
="display: none"></iframe>'); // width
: 300px
; height
: 100px
; border
: 5px solid red
5798 $iframe
.attr('name', $iframe
.wcfIdentify());
5799 $('body').append($iframe
);
5800 this._overlay
.find('form').attr('target', $iframe
.wcfIdentify());
5802 // add events (iframe onload)
5803 $iframe
.load(function() {
5804 console
.debug('iframe ready');
5805 console
.debug($iframe
.contents());
5808 this._overlay
.find('form').submit(function() {
5809 $iframe
.data('loading', true);
5810 $self
._overlay
.wcfDialog('close');
5813 this._overlay
.wcfDialog({
5815 onClose: function() {
5816 if (!$iframe
.data('loading')) {
5825 * Namespace for sortables.
5830 * Sortable implementation for lists.
5832 * @param string containerID
5833 * @param string className
5834 * @param integer offset
5835 * @param object options
5837 WCF
.Sortable
.List
= Class
.extend({
5839 * additional parameters for AJAX request
5842 _additionalParameters
: { },
5863 * notification object
5864 * @var WCF.System.Notification
5866 _notification
: null,
5882 * @var WCF.Action.Proxy
5893 * Creates a new sortable list.
5895 * @param string containerID
5896 * @param string className
5897 * @param integer offset
5898 * @param object options
5899 * @param boolean isSimpleSorting
5900 * @param object additionalParameters
5902 init: function(containerID
, className
, offset
, options
, isSimpleSorting
, additionalParameters
) {
5903 this._additionalParameters
= additionalParameters
|| { };
5904 this._containerID
= $.wcfEscapeID(containerID
);
5905 this._container
= $('#' + this._containerID
);
5906 this._className
= className
;
5907 this._offset
= (offset
) ? offset
: 0;
5908 this._proxy
= new WCF
.Action
.Proxy({
5909 success
: $.proxy(this._success
, this)
5911 this._structure
= { };
5914 this._options
= $.extend(true, {
5916 connectWith
: '#' + this._containerID
+ ' .sortableList',
5917 disableNesting
: 'sortableNoNesting',
5918 errorClass
: 'sortableInvalidTarget',
5919 forcePlaceholderSize
: true,
5921 items
: 'li:not(.sortableNoSorting)',
5923 placeholder
: 'sortablePlaceholder',
5924 tolerance
: 'pointer',
5925 toleranceElement
: '> span'
5928 if (isSimpleSorting
) {
5929 $('#' + this._containerID
+ ' .sortableList').sortable(this._options
);
5932 $('#' + this._containerID
+ ' > .sortableList').wcfNestedSortable(this._options
);
5935 if (this._className
) {
5936 this._container
.find('.formSubmit > button[data-type="submit"]').click($.proxy(this._submit
, this));
5941 * Saves object structure.
5943 _submit: function() {
5945 this._structure
= { };
5948 this._container
.find('.sortableList').each($.proxy(function(index
, list
) {
5949 var $list
= $(list
);
5950 var $parentID
= $list
.data('objectID');
5952 if ($parentID
!== undefined) {
5953 $list
.children(this._options
.items
).each($.proxy(function(index
, listItem
) {
5954 var $objectID
= $(listItem
).data('objectID');
5956 if (!this._structure
[$parentID
]) {
5957 this._structure
[$parentID
] = [ ];
5960 this._structure
[$parentID
].push($objectID
);
5966 var $parameters
= $.extend(true, {
5968 offset
: this._offset
,
5969 structure
: this._structure
5971 }, this._additionalParameters
);
5973 this._proxy
.setOption('data', {
5974 actionName
: 'updatePosition',
5975 className
: this._className
,
5976 parameters
: $parameters
5978 this._proxy
.sendRequest();
5982 * Shows notification upon success.
5984 * @param object data
5985 * @param string textStatus
5986 * @param jQuery jqXHR
5988 _success: function(data
, textStatus
, jqXHR
) {
5989 if (this._notification
=== null) {
5990 this._notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.global.form.edit.success'));
5993 this._notification
.show();
5997 WCF
.Popover
= Class
.extend({
5999 * currently active element id
6002 _activeElementID
: '',
6008 _cancelPopover
: false,
6017 * default dimensions, should reflect the estimated size
6020 _defaultDimensions
: {
6026 * default orientation, may be a combintion of left/right and bottom/top
6029 _defaultOrientation
: {
6035 * delay to show or hide popover, values in miliseconds
6044 * true, if an element is being hovered
6047 _hoverElement
: false,
6050 * element id of element being hovered
6053 _hoverElementID
: '',
6056 * true, if popover is being hovered
6059 _hoverPopover
: false,
6062 * minimum margin (all directions) for popover
6068 * periodical executer once element or popover is no longer being hovered
6069 * @var WCF.PeriodicalExecuter
6074 * periodical executer once an element is being hovered
6075 * @var WCF.PeriodicalExecuter
6077 _peOverElement
: null,
6089 _popoverContent
: null,
6093 * popover horizontal offset
6105 * Initializes a new WCF.Popover object.
6107 * @param string selector
6109 init: function(selector
) {
6110 // assign default values
6111 this._activeElementID
= '';
6112 this._cancelPopover
= false;
6114 this._defaultDimensions
= {
6118 this._defaultOrientation
= {
6126 this._hoverElement
= false;
6127 this._hoverElementID
= '';
6128 this._hoverPopover
= false;
6131 this._peOverElement
= null;
6132 this._popoverOffset
= 10;
6133 this._selector
= selector
;
6135 this._popover
= $('<div class="popover"><div class="popoverContent"></div></div>').hide().appendTo(document
.body
);
6136 this._popoverContent
= this._popover
.children('.popoverContent:eq(0)');
6137 this._popover
.hover($.proxy(this._overPopover
, this), $.proxy(this._out
, this));
6139 this._initContainers();
6140 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Popover.'+selector
, $.proxy(this._initContainers
, this));
6144 * Initializes all element triggers.
6146 _initContainers: function() {
6147 var $elements
= $(this._selector
);
6148 if (!$elements
.length
) {
6152 $elements
.each($.proxy(function(index
, element
) {
6153 var $element
= $(element
);
6154 var $elementID
= $element
.wcfIdentify();
6156 if (!this._data
[$elementID
]) {
6157 this._data
[$elementID
] = {
6162 $element
.hover($.proxy(this._overElement
, this), $.proxy(this._out
, this));
6164 if ($element
.getTagName() === 'a' && $element
.attr('href')) {
6165 $element
.click($.proxy(this._cancel
, this));
6172 * Cancels popovers if link is being clicked
6174 _cancel: function(event
) {
6175 this._cancelPopover
= true;
6180 * Triggered once an element is being hovered.
6182 * @param object event
6184 _overElement: function(event
) {
6185 if (this._cancelPopover
) {
6189 if (this._peOverElement
!== null) {
6190 this._peOverElement
.stop();
6193 var $elementID
= $(event
.currentTarget
).wcfIdentify();
6194 this._hoverElementID
= $elementID
;
6195 this._peOverElement
= new WCF
.PeriodicalExecuter($.proxy(function(pe
) {
6198 // still above the same element
6199 if (this._hoverElementID
=== $elementID
) {
6200 this._activeElementID
= $elementID
;
6203 }, this), this._delay
.show
);
6205 this._hoverElement
= true;
6206 this._hoverPopover
= false;
6210 * Prepares popover to be displayed.
6212 _prepare: function() {
6213 if (this._cancelPopover
) {
6217 if (this._peOut
!== null) {
6222 if (this._popover
.is(':visible')) {
6227 if (!this._data
[this._activeElementID
].loading
&& this._data
[this._activeElementID
].content
) {
6228 WCF
.DOMNodeInsertedHandler
.enable();
6230 this._popoverContent
.html(this._data
[this._activeElementID
].content
);
6232 WCF
.DOMNodeInsertedHandler
.disable();
6235 this._data
[this._activeElementID
].loading
= true;
6239 var $dimensions
= this._popover
.show().getDimensions();
6240 if (this._data
[this._activeElementID
].loading
) {
6242 height
: Math
.max($dimensions
.height
, this._defaultDimensions
.height
),
6243 width
: Math
.max($dimensions
.width
, this._defaultDimensions
.width
)
6247 $dimensions
= this._fixElementDimensions(this._popover
, $dimensions
);
6249 this._popover
.hide();
6252 var $orientation
= this._getOrientation($dimensions
.height
, $dimensions
.width
);
6253 this._popover
.css(this._getCSS($orientation
.x
, $orientation
.y
));
6255 // apply orientation to popover
6256 this._popover
.removeClass('bottom left right top').addClass($orientation
.x
).addClass($orientation
.y
);
6262 * Displays the popover.
6265 if (this._cancelPopover
) {
6269 this._popover
.stop().show().css({ opacity
: 1 }).wcfFadeIn();
6271 if (this._data
[this._activeElementID
].loading
) {
6272 this._loadContent();
6275 this._popoverContent
.css({ opacity
: 1 });
6280 * Loads content, should be overwritten by child classes.
6282 _loadContent: function() { },
6285 * Inserts content and animating transition.
6287 * @param string elementID
6288 * @param boolean animate
6290 _insertContent: function(elementID
, content
, animate
) {
6291 this._data
[elementID
] = {
6296 // only update content if element id is active
6297 if (this._activeElementID
=== elementID
) {
6298 WCF
.DOMNodeInsertedHandler
.enable();
6301 // get current dimensions
6302 var $dimensions
= this._popoverContent
.getDimensions();
6304 // insert new content
6305 this._popoverContent
.css({
6309 this._popoverContent
.html(this._data
[elementID
].content
);
6310 var $newDimensions
= this._popoverContent
.getDimensions();
6312 // enforce current dimensions and remove HTML
6313 this._popoverContent
.html('').css({
6314 height
: $dimensions
.height
+ 'px',
6315 width
: $dimensions
.width
+ 'px'
6318 // animate to new dimensons
6320 this._popoverContent
.animate({
6321 height
: $newDimensions
.height
+ 'px',
6322 width
: $newDimensions
.width
+ 'px'
6323 }, 300, function() {
6324 WCF
.DOMNodeInsertedHandler
.enable();
6326 self
._popoverContent
.html(self
._data
[elementID
].content
).css({ opacity
: 0 }).animate({ opacity
: 1 }, 200);
6328 WCF
.DOMNodeInsertedHandler
.disable();
6332 // insert new content
6333 this._popoverContent
.html(this._data
[elementID
].content
);
6336 WCF
.DOMNodeInsertedHandler
.disable();
6341 * Hides the popover.
6343 _hide: function(disableAnimation
) {
6345 this._popoverContent
.stop();
6346 this._popover
.stop();
6348 if (disableAnimation
) {
6349 self
._popover
.css({ opacity
: 0 }).hide();
6350 self
._popoverContent
.empty().css({ height
: 'auto', opacity
: 0, width
: 'auto' });
6353 this._popover
.wcfFadeOut(function() {
6354 self
._popoverContent
.empty().css({ height
: 'auto', opacity
: 0, width
: 'auto' });
6355 self
._popover
.hide();
6361 * Triggered once popover is being hovered.
6363 _overPopover: function() {
6364 if (this._peOut
!== null) {
6368 this._hoverElement
= false;
6369 this._hoverPopover
= true;
6373 * Triggered once element *or* popover is now longer hovered.
6375 _out: function(event
) {
6376 if (this._cancelPopover
) {
6380 this._hoverElementID
= '';
6381 this._hoverElement
= false;
6382 this._hoverPopover
= false;
6384 this._peOut
= new WCF
.PeriodicalExecuter($.proxy(function(pe
) {
6387 // hide popover is neither element nor popover was hovered given time
6388 if (!this._hoverElement
&& !this._hoverPopover
) {
6391 }, this), this._delay
.hide
);
6395 * Resolves popover orientation, tries to use default orientation first.
6397 * @param integer height
6398 * @param integer width
6401 _getOrientation: function(height
, width
) {
6402 // get offsets and dimensions
6403 var $element
= $('#' + this._activeElementID
);
6404 var $offsets
= $element
.getOffsets('offset');
6405 var $elementDimensions
= $element
.getDimensions();
6406 var $documentDimensions
= $(document
).getDimensions();
6408 // try default orientation first
6409 var $orientationX
= (this._defaultOrientation
.x
=== 'left') ? 'left' : 'right';
6410 var $orientationY
= (this._defaultOrientation
.y
=== 'bottom') ? 'bottom' : 'top';
6411 var $result
= this._evaluateOrientation($orientationX
, $orientationY
, $offsets
, $elementDimensions
, $documentDimensions
, height
, width
);
6413 if ($result
.flawed
) {
6414 // try flipping orientationX
6415 $orientationX
= ($orientationX
=== 'left') ? 'right' : 'left';
6416 $result
= this._evaluateOrientation($orientationX
, $orientationY
, $offsets
, $elementDimensions
, $documentDimensions
, height
, width
);
6418 if ($result
.flawed
) {
6419 // try flipping orientationY while maintaing original orientationX
6420 $orientationX
= ($orientationX
=== 'right') ? 'left' : 'right';
6421 $orientationY
= ($orientationY
=== 'bottom') ? 'top' : 'bottom';
6422 $result
= this._evaluateOrientation($orientationX
, $orientationY
, $offsets
, $elementDimensions
, $documentDimensions
, height
, width
);
6424 if ($result
.flawed
) {
6425 // try flipping both orientationX and orientationY compared to default values
6426 $orientationX
= ($orientationX
=== 'left') ? 'right' : 'left';
6427 $result
= this._evaluateOrientation($orientationX
, $orientationY
, $offsets
, $elementDimensions
, $documentDimensions
, height
, width
);
6429 if ($result
.flawed
) {
6430 // fuck this shit, we will use the default orientation
6431 $orientationX
= (this._defaultOrientationX
=== 'left') ? 'left' : 'right';
6432 $orientationY
= (this._defaultOrientationY
=== 'bottom') ? 'bottom' : 'top';
6445 * Evaluates if popover fits into given orientation.
6447 * @param string orientationX
6448 * @param string orientationY
6449 * @param object offsets
6450 * @param object elementDimensions
6451 * @param object documentDimensions
6452 * @param integer height
6453 * @param integer width
6456 _evaluateOrientation: function(orientationX
, orientationY
, offsets
, elementDimensions
, documentDimensions
, height
, width
) {
6457 var $heightDifference
= 0, $widthDifference
= 0;
6458 switch (orientationX
) {
6460 $widthDifference
= offsets
.left
- width
;
6464 $widthDifference
= documentDimensions
.width
- (offsets
.left
+ width
);
6468 switch (orientationY
) {
6470 $heightDifference
= documentDimensions
.height
- (offsets
.top
+ elementDimensions
.height
+ this._popoverOffset
+ height
);
6474 $heightDifference
= offsets
.top
- (height
- this._popoverOffset
);
6478 // check if both difference are above margin
6479 var $flawed
= false;
6480 if ($heightDifference
< this._margin
|| $widthDifference
< this._margin
) {
6486 x
: $widthDifference
,
6487 y
: $heightDifference
6492 * Computes CSS for popover.
6494 * @param string orientationX
6495 * @param string orientationY
6498 _getCSS: function(orientationX
, orientationY
) {
6506 var $element
= $('#' + this._activeElementID
);
6507 var $offsets
= $element
.getOffsets('offset');
6508 var $elementDimensions
= this._fixElementDimensions($element
, $element
.getDimensions());
6509 var $windowDimensions
= $(window
).getDimensions();
6511 switch (orientationX
) {
6513 $css
.right
= $windowDimensions
.width
- ($offsets
.left
+ $elementDimensions
.width
);
6517 $css
.left
= $offsets
.left
;
6521 switch (orientationY
) {
6523 $css
.top
= $offsets
.top
+ ($elementDimensions
.height
+ this._popoverOffset
);
6527 $css
.bottom
= $windowDimensions
.height
- ($offsets
.top
- this._popoverOffset
);
6535 * Tries to fix dimensions if element is partially hidden (overflow: hidden).
6537 * @param jQuery element
6538 * @param object dimensions
6539 * @return dimensions
6541 _fixElementDimensions: function(element
, dimensions
) {
6542 var $parentDimensions
= element
.parent().getDimensions();
6544 if ($parentDimensions
.height
< dimensions
.height
) {
6545 dimensions
.height
= $parentDimensions
.height
;
6548 if ($parentDimensions
.width
< dimensions
.width
) {
6549 dimensions
.width
= $parentDimensions
.width
;
6557 * Provides an extensible item list with built-in search.
6559 * @param string itemListSelector
6560 * @param string searchInputSelector
6562 WCF
.EditableItemList
= Class
.extend({
6564 * allows custom input not recognized by search to be added
6567 _allowCustomInput
: false,
6576 * internal data storage
6588 * item list container
6607 * @var WCF.Search.Base
6612 * search input element
6618 * Creates a new WCF.EditableItemList object.
6620 * @param string itemListSelector
6621 * @param string searchInputSelector
6623 init: function(itemListSelector
, searchInputSelector
) {
6624 this._itemList
= $(itemListSelector
);
6625 this._searchInput
= $(searchInputSelector
);
6627 if (!this._itemList
.length
|| !this._searchInput
.length
) {
6628 console
.debug("[WCF.EditableItemList] Item list and/or search input do not exist, aborting.");
6632 this._objectID
= this._getObjectID();
6633 this._objectTypeID
= this._getObjectTypeID();
6635 // bind item listener
6636 this._itemList
.find('.jsEditableItem').click($.proxy(this._click
, this));
6639 if (!this._itemList
.children('ul').length
) {
6640 $('<ul />').appendTo(this._itemList
);
6642 this._itemList
= this._itemList
.children('ul');
6645 this._form
= this._itemList
.parents('form').submit($.proxy(this._submit
, this));
6647 if (this._allowCustomInput
) {
6648 this._searchInput
.keydown($.proxy(this._keyDown
, this));
6653 * Handles the key down event.
6655 * @param object event
6657 _keyDown: function(event
) {
6658 if (event
=== null || (event
.which
=== 13 || event
.which
=== 188)) {
6659 var $value
= $.trim(this._searchInput
.val());
6660 if ($value
=== '') {
6670 this._searchInput
.val('');
6672 if (event
!== null) {
6673 event
.stopPropagation();
6683 * Loads raw data and converts it into internal structure. Override this methods
6684 * in your derived classes.
6686 * @param object data
6688 load: function(data
) { },
6691 * Removes an item on click.
6693 * @param object event
6696 _click: function(event
) {
6697 var $element
= $(event
.currentTarget
);
6698 var $objectID
= $element
.data('objectID');
6699 var $label
= $element
.data('label');
6702 this._search
.removeExcludedSearchValue($label
);
6704 this._removeItem($objectID
, $label
);
6708 event
.stopPropagation();
6713 * Returns current object id.
6717 _getObjectID: function() {
6722 * Returns current object type id.
6726 _getObjectTypeID: function() {
6731 * Adds a new item to the list.
6733 * @param object data
6736 addItem: function(data
) {
6737 if (this._data
[data
.objectID
]) {
6738 if (!(data
.objectID
=== 0 && this._allowCustomInput
)) {
6743 var $listItem
= $('<li class="badge">' + data
.label
+ '</li>').data('objectID', data
.objectID
).data('label', data
.label
).appendTo(this._itemList
);
6744 $listItem
.click($.proxy(this._click
, this));
6747 this._search
.addExcludedSearchValue(data
.label
);
6749 this._addItem(data
.objectID
, data
.label
);
6755 * Handles form submit, override in your class.
6757 _submit: function() {
6758 this._keyDown(null);
6762 * Adds an item to internal storage.
6764 * @param integer objectID
6765 * @param string label
6767 _addItem: function(objectID
, label
) {
6768 this._data
[objectID
] = label
;
6772 * Removes an item from internal storage.
6774 * @param integer objectID
6775 * @param string label
6777 _removeItem: function(objectID
, label
) {
6778 delete this._data
[objectID
];
6783 * Provides a generic sitemap.
6785 WCF
.Sitemap
= Class
.extend({
6787 * sitemap name cache
6799 * initialization state
6806 * @var WCF.Action.Proxy
6811 * Initializes the generic sitemap.
6814 $('#sitemap').click($.proxy(this._click
, this));
6817 this._dialog
= null;
6818 this._didInit
= false;
6819 this._proxy
= new WCF
.Action
.Proxy({
6820 success
: $.proxy(this._success
, this)
6825 * Handles clicks on the sitemap icon.
6827 _click: function() {
6828 if (this._dialog
=== null) {
6829 this._dialog
= $('<div id="sitemapDialog" />').appendTo(document
.body
);
6831 this._proxy
.setOption('data', {
6832 actionName
: 'getSitemap',
6833 className
: 'wcf\\data\\sitemap\\SitemapAction'
6835 this._proxy
.sendRequest();
6838 this._dialog
.wcfDialog('open');
6843 * Handles successful AJAX responses.
6845 * @param object data
6846 * @param string textStatus
6847 * @param jQuery jqXHR
6849 _success: function(data
, textStatus
, jqXHR
) {
6850 if (this._didInit
) {
6851 this._cache
.push(data
.returnValues
.sitemapName
);
6853 this._dialog
.find('#sitemap_' + data
.returnValues
.sitemapName
).html(data
.returnValues
.template
);
6856 this._dialog
.wcfDialog('render');
6859 // mark sitemap name as loaded
6860 this._cache
.push(data
.returnValues
.sitemapName
);
6862 // insert sitemap template
6863 this._dialog
.html(data
.returnValues
.template
);
6865 // bind event listener
6866 this._dialog
.find('.sitemapNavigation').click($.proxy(this._navigate
, this));
6869 this._dialog
.wcfDialog({
6870 title
: WCF
.Language
.get('wcf.sitemap.title')
6873 this._didInit
= true;
6878 * Navigates between different sitemaps.
6880 * @param object event
6882 _navigate: function(event
) {
6883 var $sitemapName
= $(event
.currentTarget
).data('sitemapName');
6884 if (WCF
.inArray($sitemapName
, this._cache
)) {
6885 this._dialog
.find('.tabMenuContainer').wcfTabs('select', 'sitemap_' + $sitemapName
);
6888 this._dialog
.wcfDialog('render');
6891 this._proxy
.setOption('data', {
6892 actionName
: 'getSitemap',
6893 className
: 'wcf\\data\\sitemap\\SitemapAction',
6895 sitemapName
: $sitemapName
6898 this._proxy
.sendRequest();
6904 * Provides a language chooser.
6906 * @param string containerID
6907 * @param string inputFieldID
6908 * @param integer languageID
6909 * @param object languages
6910 * @param object callback
6912 WCF
.Language
.Chooser
= Class
.extend({
6932 * Initializes the language chooser.
6934 * @param string containerID
6935 * @param string inputFieldID
6936 * @param integer languageID
6937 * @param object languages
6938 * @param object callback
6939 * @param boolean allowEmptyValue
6941 init: function(containerID
, inputFieldID
, languageID
, languages
, callback
, allowEmptyValue
) {
6942 var $container
= $('#' + containerID
);
6943 if ($container
.length
!= 1) {
6944 console
.debug("[WCF.Language.Chooser] Invalid container id '" + containerID
+ "' given");
6948 // bind language id input
6949 this._input
= $('#' + inputFieldID
);
6950 if (!this._input
.length
) {
6951 this._input
= $('<input type="hidden" name="' + inputFieldID
+ '" value="' + languageID
+ '" />').appendTo($container
);
6955 if (callback
!== undefined) {
6956 if (!$.isFunction(callback
)) {
6957 console
.debug("[WCF.Language.Chooser] Given callback is invalid");
6961 this._callback
= callback
;
6964 // create language dropdown
6965 this._dropdown
= $('<div class="dropdown" id="' + containerID
+ '-languageChooser" />').appendTo($container
);
6966 $('<div class="dropdownToggle boxFlag box24" data-toggle="' + containerID
+ '-languageChooser"></div>').appendTo(this._dropdown
);
6967 var $dropdownMenu
= $('<ul class="dropdownMenu" />').appendTo(this._dropdown
);
6969 for (var $languageID
in languages
) {
6970 var $language
= languages
[$languageID
];
6971 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
);
6972 $item
.data('languageID', $languageID
).click($.proxy(this._click
, this));
6974 // update dropdown label
6975 if ($languageID
== languageID
) {
6976 var $html
= $('' + $item
.html());
6977 var $innerContent
= $html
.children().detach();
6978 this._dropdown
.children('.dropdownToggle').empty().append($innerContent
);
6982 // allow an empty selection (e.g. using as language filter)
6983 if (allowEmptyValue
) {
6984 $('<li class="dropdownDivider" />').appendTo($dropdownMenu
);
6985 var $item
= $('<li><a>' + WCF
.Language
.get('wcf.global.language.noSelection') + '</a></li>').data('languageID', 0).click($.proxy(this._click
, this)).appendTo($dropdownMenu
);
6987 if (languageID
=== 0) {
6988 this._dropdown
.children('.dropdownToggle').empty().append($item
.html());
6992 WCF
.Dropdown
.init();
6996 * Handles click events.
6998 * @param object event
7000 _click: function(event
) {
7001 var $item
= $(event
.currentTarget
);
7002 var $languageID
= $item
.data('languageID');
7004 // update input field
7005 this._input
.val($languageID
);
7007 // update dropdown label
7008 var $html
= $('' + $item
.html());
7009 var $innerContent
= ($languageID
=== 0) ? $html
: $html
.children().detach();
7010 this._dropdown
.children('.dropdownToggle').empty().append($innerContent
);
7013 if (this._callback
!== null) {
7014 this._callback($item
);
7020 * Namespace for style related classes.
7025 * Provides a visual style chooser.
7027 WCF
.Style
.Chooser
= Class
.extend({
7036 * @var WCF.Action.Proxy
7041 * Initializes the style chooser class.
7044 $('<li class="styleChooser"><a>' + WCF
.Language
.get('wcf.style.changeStyle') + '</a></li>').appendTo($('#footerNavigation > ul.navigationItems')).click($.proxy(this._showDialog
, this));
7046 this._proxy
= new WCF
.Action
.Proxy({
7047 success
: $.proxy(this._success
, this)
7052 * Displays the style chooser dialog.
7054 _showDialog: function() {
7055 if (this._dialog
=== null) {
7056 this._dialog
= $('<div id="styleChooser" />').hide().appendTo(document
.body
);
7060 this._dialog
.wcfDialog({
7061 title
: WCF
.Language
.get('wcf.style.changeStyle')
7067 * Loads the style chooser dialog.
7069 _loadDialog: function() {
7070 this._proxy
.setOption('data', {
7071 actionName
: 'getStyleChooser',
7072 className
: 'wcf\\data\\style\\StyleAction'
7074 this._proxy
.sendRequest();
7078 * Handles successful AJAX requests.
7080 * @param object data
7081 * @param string textStatus
7082 * @param jQuery jqXHR
7084 _success: function(data
, textStatus
, jqXHR
) {
7085 if (data
.returnValues
.actionName
=== 'changeStyle') {
7086 window
.location
.reload();
7090 this._dialog
.html(data
.returnValues
.template
);
7091 this._dialog
.find('li').addClass('pointer').click($.proxy(this._click
, this));
7097 * Changes user style.
7099 * @param object event
7101 _click: function(event
) {
7102 this._proxy
.setOption('data', {
7103 actionName
: 'changeStyle',
7104 className
: 'wcf\\data\\style\\StyleAction',
7105 objectIDs
: [ $(event
.currentTarget
).data('styleID') ]
7107 this._proxy
.sendRequest();
7112 * Converts static user panel items into interactive dropdowns.
7114 * @param string containerID
7116 WCF
.UserPanel
= Class
.extend({
7124 * initialization state
7130 * original link element
7136 * reverts to original link if return values are empty
7139 _revertOnEmpty
: true,
7142 * Initialites the WCF.UserPanel class.
7144 * @param string containerID
7146 init: function(containerID
) {
7147 this._container
= $('#' + containerID
);
7148 this._didLoad
= false;
7149 this._revertOnEmpty
= true;
7151 if (this._container
.length
!= 1) {
7152 console
.debug("[WCF.UserPanel] Unable to find container identfied by '" + containerID
+ "', aborting.");
7156 if (this._container
.data('count')) {
7157 WCF
.DOMNodeInsertedHandler
.enable();
7159 WCF
.DOMNodeInsertedHandler
.disable();
7164 * Converts link into an interactive dropdown menu.
7166 _convert: function() {
7167 this._container
.addClass('dropdown');
7168 this._link
= this._container
.children('a').remove();
7170 $('<a class="dropdownToggle jsTooltip" title="' + this._container
.data('title') + '">' + this._link
.html() + '</a>').appendTo(this._container
).click($.proxy(this._click
, this));
7171 var $dropdownMenu
= $('<ul class="dropdownMenu" />').appendTo(this._container
);
7172 $('<li class="jsDropdownPlaceholder"><span>' + WCF
.Language
.get('wcf.global.loading') + '</span></li>').appendTo($dropdownMenu
);
7174 this._addDefaultItems($dropdownMenu
);
7178 * Adds default items to dropdown menu.
7180 * @param jQuery dropdownMenu
7182 _addDefaultItems: function(dropdownMenu
) { },
7185 * Adds a dropdown divider.
7187 * @param jQuery dropdownMenu
7189 _addDivider: function(dropdownMenu
) {
7190 $('<li class="dropdownDivider" />').appendTo(dropdownMenu
);
7194 * Handles clicks on the dropdown item.
7196 _click: function() {
7197 if (this._didLoad
) {
7201 new WCF
.Action
.Proxy({
7203 data
: this._getParameters(),
7204 success
: $.proxy(this._success
, this)
7207 this._didLoad
= true;
7211 * Returns a list of parameters for AJAX request.
7215 _getParameters: function() {
7220 * Handles successful AJAX requests.
7222 * @param object data
7223 * @param string textStatus
7224 * @param jQuery jqXHR
7226 _success: function(data
, textStatus
, jqXHR
) {
7227 if (data
.returnValues
&& data
.returnValues
.template
) {
7228 var $dropdownMenu
= this._container
.children('.dropdownMenu');
7229 $dropdownMenu
.children('.jsDropdownPlaceholder').remove();
7230 $('' + data
.returnValues
.template
).prependTo($dropdownMenu
);
7233 this._container
.removeClass('dropdown').empty();
7234 this._link
.appendTo(this._container
);
7237 this._container
.find('.badge').remove();
7243 * WCF implementation for nested sortables.
7245 $.widget("ui.wcfNestedSortable", $.extend({}, $.mjs
.nestedSortable
.prototype, {
7246 _clearEmpty: function(item
) {
7247 /* Does nothing because we want to keep empty lists */
7252 * WCF implementation for dialogs, based upon ideas by jQuery UI.
7254 $.widget('ui.wcfDialog', {
7274 * dialog content dimensions
7277 _contentDimensions
: null,
7283 _isRendering
: false,
7292 * plain html for title
7304 * dialog visibility state
7317 closeButtonLabel
: null,
7326 showLoadingOverlay
: true,
7329 url
: 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN
+ SID_ARG_2ND
,
7337 * Initializes a new dialog.
7340 if (this.options
.ajax
) {
7341 new WCF
.Action
.Proxy({
7343 data
: this.options
.data
,
7344 showLoadingOverlay
: this.options
.showLoadingOverlay
,
7345 success
: $.proxy(this._success
, this),
7346 type
: this.options
.type
,
7347 url
: this.options
.url
7350 // force open if using AJAX
7351 this.options
.autoOpen
= true;
7353 // apply loading overlay
7354 this._content
.addClass('overlayLoading');
7357 if (this.options
.autoOpen
) {
7362 $(window
).resize($.proxy(this._resize
, this));
7366 * Creates a new dialog instance.
7368 _create: function() {
7369 if (this.options
.closeButtonLabel
=== null) {
7370 this.options
.closeButtonLabel
= WCF
.Language
.get('wcf.global.button.close');
7373 WCF
.DOMNodeInsertedHandler
.enable();
7375 // create dialog container
7376 this._container
= $('<div class="dialogContainer" />').hide().css({ zIndex
: this.options
.zIndex
}).appendTo(document
.body
);
7377 this._titlebar
= $('<header class="dialogTitlebar" />').hide().appendTo(this._container
);
7378 this._title
= $('<span class="dialogTitle" />').hide().appendTo(this._titlebar
);
7379 this._closeButton
= $('<a class="dialogCloseButton jsTooltip" title="' + this.options
.closeButtonLabel
+ '"><span /></a>').click($.proxy(this.close
, this)).hide().appendTo(this._titlebar
);
7380 this._content
= $('<div class="dialogContent" />').appendTo(this._container
);
7382 this._setOption('title', this.options
.title
);
7383 this._setOption('closable', this.options
.closable
);
7385 // move target element into content
7386 var $content
= this.element
.detach();
7387 this._content
.html($content
);
7389 // create modal view
7390 if (this.options
.modal
) {
7391 this._overlay
= $('#jsWcfDialogOverlay');
7392 if (!this._overlay
.length
) {
7393 this._overlay
= $('<div id="jsWcfDialogOverlay" class="dialogOverlay" />').css({ height
: '100%', zIndex
: 399 }).appendTo(document
.body
);
7396 if (this.options
.closable
) {
7397 this._overlay
.click($.proxy(this.close
, this));
7399 $(document
).keyup($.proxy(function(event
) {
7400 if (event
.keyCode
&& event
.keyCode
=== $.ui
.keyCode
.ESCAPE
) {
7402 event
.preventDefault();
7408 WCF
.DOMNodeInsertedHandler
.disable();
7412 * Sets the given option to the given value.
7413 * See the jQuery UI widget documentation for more.
7415 _setOption: function(key
, value
) {
7416 this.options
[key
] = value
;
7418 if (key
== 'hideTitle' || key
== 'title') {
7419 if (!this.options
.hideTitle
&& this.options
.title
!= '') {
7420 this._title
.html(this.options
.title
).show();
7422 this._title
.html('');
7424 } else if (key
== 'closable' || key
== 'closeButtonLabel') {
7425 if (this.options
.closable
) {
7426 WCF
.DOMNodeInsertedHandler
.enable();
7428 this._closeButton
.attr('title', this.options
.closeButtonLabel
).show().find('span').html(this.options
.closeButtonLabel
);
7430 WCF
.DOMNodeInsertedHandler
.disable();
7432 this._closeButton
.hide();
7436 if ((!this.options
.hideTitle
&& this.options
.title
!= '') || this.options
.closable
) {
7437 this._titlebar
.show();
7439 this._titlebar
.hide();
7446 * Handles successful AJAX requests.
7448 * @param object data
7449 * @param string textStatus
7450 * @param jQuery jqXHR
7452 _success: function(data
, textStatus
, jqXHR
) {
7454 // initialize dialog content
7455 this._initDialog(data
);
7457 // remove loading overlay
7458 this._content
.removeClass('overlayLoading');
7460 if (this.options
.success
!== null && $.isFunction(this.options
.success
)) {
7461 this.options
.success(data
, textStatus
, jqXHR
);
7467 * Initializes dialog content if applicable.
7469 * @param object data
7471 _initDialog: function(data
) {
7473 if (this._getResponseValue(data
, 'template')) {
7474 this._content
.children().html(this._getResponseValue(data
, 'template'));
7479 if (this._getResponseValue(data
, 'title')) {
7480 this._setOption('title', this._getResponseValue(data
, 'title'));
7485 * Returns a response value, taking care of different object
7486 * structure returned by AJAXProxy.
7488 * @param object data
7491 _getResponseValue: function(data
, key
) {
7492 if (data
.returnValues
&& data
.returnValues
[key
]) {
7493 return data
.returnValues
[key
];
7495 else if (data
[key
]) {
7503 * Opens this dialog.
7506 if (this.isOpen()) {
7510 if (this._overlay
!== null) {
7511 WCF
.activeDialogs
++;
7513 if (WCF
.activeDialogs
=== 1) {
7514 this._overlay
.show();
7519 this._isOpen
= true;
7523 * Returns true, if dialog is visible.
7527 isOpen: function() {
7528 return this._isOpen
;
7532 * Closes this dialog.
7535 if (!this.isOpen() || !this.options
.closable
) {
7539 this._isOpen
= false;
7540 this._container
.wcfFadeOut();
7542 if (this._overlay
!== null) {
7543 WCF
.activeDialogs
--;
7545 if (WCF
.activeDialogs
=== 0) {
7546 this._overlay
.hide();
7550 if (this.options
.onClose
!== null) {
7551 this.options
.onClose();
7556 * Renders dialog on resize if visible.
7558 _resize: function() {
7559 if (this.isOpen()) {
7565 * Renders this dialog, should be called whenever content is updated.
7567 render: function() {
7568 if (!this.isOpen()) {
7569 // temporarily display container
7570 this._container
.show();
7573 // remove fixed content dimensions for calculation
7580 // force content to be visible
7581 this._content
.children().each(function() {
7585 // handle multiple rendering requests
7586 if (this._isRendering
) {
7587 // stop current process
7588 this._container
.stop();
7589 this._content
.stop();
7591 // set dialog to be fully opaque, should prevent weird bugs in WebKit
7592 this._container
.show().css('opacity', 1.0);
7595 if (this._content
.find('.formSubmit').length
) {
7596 this._content
.addClass('dialogForm');
7599 this._content
.removeClass('dialogForm');
7602 // calculate dimensions
7603 var $windowDimensions
= $(window
).getDimensions();
7604 var $containerDimensions
= this._container
.getDimensions('outer');
7605 var $contentDimensions
= this._content
.getDimensions();
7607 // calculate maximum content height
7608 var $heightDifference
= $containerDimensions
.height
- $contentDimensions
.height
;
7609 var $maximumHeight
= $windowDimensions
.height
- $heightDifference
- 120;
7610 this._content
.css({ maxHeight
: $maximumHeight
+ 'px' });
7612 // re-caculate values if container height was previously limited
7613 if ($maximumHeight
< $contentDimensions
.height
) {
7614 $containerDimensions
= this._container
.getDimensions('outer');
7617 // handle multiple rendering requests
7618 if (this._isRendering
) {
7619 // use current dimensions as previous ones
7620 this._contentDimensions
= this._getContentDimensions($maximumHeight
);
7623 // calculate new dimensions
7624 $contentDimensions
= this._getContentDimensions($maximumHeight
);
7627 var $leftOffset
= Math
.round(($windowDimensions
.width
- $containerDimensions
.width
) / 2);
7628 var $topOffset
= Math
.round(($windowDimensions
.height
- $containerDimensions
.height
) / 2);
7630 // place container at 20% height if possible
7631 var $desiredTopOffset
= Math
.round(($windowDimensions
.height
/ 100) * 20);
7632 if ($desiredTopOffset
< $topOffset
) {
7633 $topOffset
= $desiredTopOffset
;
7636 if (!this.isOpen()) {
7637 // hide container again
7638 this._container
.hide();
7641 this._container
.css({
7642 left
: $leftOffset
+ 'px',
7643 top
: $topOffset
+ 'px'
7646 // save current dimensions
7647 this._contentDimensions
= $contentDimensions
;
7651 height
: this._contentDimensions
.height
+ 'px',
7652 width
: this._contentDimensions
.width
+ 'px'
7655 // fade in container
7656 this._container
.wcfFadeIn($.proxy(function() {
7657 this._isRendering
= false;
7661 // save reference (used in callback)
7662 var $content
= this._content
;
7664 // force previous dimensions
7666 height
: this._contentDimensions
.height
+ 'px',
7667 width
: this._contentDimensions
.width
+ 'px'
7670 // apply new dimensions
7672 height
: ($contentDimensions
.height
) + 'px',
7673 width
: ($contentDimensions
.width
) + 'px'
7674 }, 300, function() {
7675 // remove static dimensions
7682 // store new dimensions
7683 this._contentDimensions
= $contentDimensions
;
7686 this._isRendering
= true;
7687 this._container
.animate({
7688 left
: $leftOffset
+ 'px',
7689 top
: $topOffset
+ 'px'
7690 }, 300, $.proxy(function() {
7691 this._isRendering
= false;
7695 if (this.options
.onShow
!== null) {
7696 this.options
.onShow();
7701 * Returns calculated content dimensions.
7703 * @param integer maximumHeight
7706 _getContentDimensions: function(maximumHeight
) {
7707 var $contentDimensions
= this._content
.getDimensions();
7709 // set height to maximum height if exceeded
7710 if ($contentDimensions
.height
> maximumHeight
) {
7711 $contentDimensions
.height
= maximumHeight
;
7714 return $contentDimensions
;
7719 * Custom tab menu implementation for WCF.
7721 $.widget('ui.wcfTabs', $.ui
.tabs
, {
7723 * Workaround for ids containing a dot ".", until jQuery UI devs learn
7724 * to properly escape ids ... (it took 18 months until they finally
7727 * @see http://bugs.jqueryui.com/ticket/4681
7728 * @see $.ui.tabs.prototype._sanitizeSelector()
7730 _sanitizeSelector: function(hash
) {
7731 return hash
.replace(/([:\.])/g, '\\$1');
7735 * @see $.ui.tabs.prototype.select()
7737 select: function(index
) {
7738 if (!$.isNumeric(index
)) {
7739 // panel identifier given
7740 this.panels
.each(function(i
, panel
) {
7741 if ($(panel
).wcfIdentify() === index
) {
7747 // unable to identify panel
7748 if (!$.isNumeric(index
)) {
7749 console
.debug("[ui.wcfTabs] Unable to find panel identified by '" + index
+ "', aborting.");
7754 $.ui
.tabs
.prototype.select
.apply(this, arguments
);
7758 * Returns the currently selected tab index.
7762 getCurrentIndex: function() {
7763 return this.lis
.index(this.lis
.filter('.ui-tabs-selected'));
7767 * Returns true, if identifier is used by an anchor.
7769 * @param string identifier
7770 * @param boolean isChildren
7773 hasAnchor: function(identifier
, isChildren
) {
7774 var $matches
= false;
7776 this.anchors
.each(function(index
, anchor
) {
7777 var $href
= $(anchor
).attr('href');
7778 if (/#.+/.test($href
)) {
7780 var $parts
= $href
.split('#', 2);
7782 $parts
= $parts
[1].split('-', 2);
7785 if ($parts
[1] === identifier
) {
7798 * Shows default tab.
7800 revertToDefault: function() {
7801 var $active
= this.element
.data('active');
7802 if (!$active
|| $active
=== '') $active
= 0;
7804 this.select($active
);
7809 * jQuery widget implementation of the wcf pagination.
7811 $.widget('ui.wcfPages', {
7822 arrowDownIcon
: null,
7826 // we use options here instead of language variables, because the paginator is not only usable with pages
7832 * Creates the pages widget.
7834 _create: function() {
7835 if (this.options
.nextPage
=== null) this.options
.nextPage
= WCF
.Language
.get('wcf.global.page.next');
7836 if (this.options
.previousPage
=== null) this.options
.previousPage
= WCF
.Language
.get('wcf.global.page.previous');
7837 if (this.options
.previousIcon
=== null) this.options
.previousIcon
= WCF
.Icon
.get('wcf.icon.circleArrowLeft');
7838 if (this.options
.nextIcon
=== null) this.options
.nextIcon
= WCF
.Icon
.get('wcf.icon.circleArrowRight');
7839 if (this.options
.arrowDownIcon
=== null) this.options
.arrowDownIcon
= WCF
.Icon
.get('wcf.icon.arrowDown');
7841 this.element
.addClass('pageNavigation');
7847 * Destroys the pages widget.
7849 destroy: function() {
7850 $.Widget
.prototype.destroy
.apply(this, arguments
);
7852 this.element
.children().remove();
7856 * Renders the pages widget.
7858 _render: function() {
7859 // only render if we have more than 1 page
7860 if (!this.options
.disabled
&& this.options
.maxPage
> 1) {
7861 var $hasHiddenPages
= false;
7863 // make sure pagination is visible
7864 if (this.element
.hasClass('hidden')) {
7865 this.element
.removeClass('hidden');
7867 this.element
.show();
7869 this.element
.children().remove();
7871 var $pageList
= $('<ul></ul>');
7872 this.element
.append($pageList
);
7874 var $previousElement
= $('<li></li>').addClass('button skip');
7875 $pageList
.append($previousElement
);
7877 if (this.options
.activePage
> 1) {
7878 var $previousLink
= $('<a' + ((this.options
.previousPage
!= null) ? (' title="' + this.options
.previousPage
+ '"') : ('')) + '></a>');
7879 $previousElement
.append($previousLink
);
7880 this._bindSwitchPage($previousLink
, this.options
.activePage
- 1);
7882 var $previousImage
= $('<img src="' + this.options
.previousIcon
+ '" alt="" />');
7883 $previousLink
.append($previousImage
);
7886 var $previousImage
= $('<img src="' + this.options
.previousIcon
+ '" alt="" />');
7887 $previousElement
.append($previousImage
);
7888 $previousElement
.addClass('disabled');
7889 $previousImage
.addClass('disabled');
7891 $previousImage
.addClass('icon16');
7894 $pageList
.append(this._renderLink(1));
7896 // calculate page links
7897 var $maxLinks
= this.SHOW_LINKS
- 4;
7898 var $linksBefore
= this.options
.activePage
- 2;
7899 if ($linksBefore
< 0) $linksBefore
= 0;
7900 var $linksAfter
= this.options
.maxPage
- (this.options
.activePage
+ 1);
7901 if ($linksAfter
< 0) $linksAfter
= 0;
7902 if (this.options
.activePage
> 1 && this.options
.activePage
< this.options
.maxPage
) $maxLinks
--;
7904 var $half
= $maxLinks
/ 2;
7905 var $left
= this.options
.activePage
;
7906 var $right
= this.options
.activePage
;
7907 if ($left
< 1) $left
= 1;
7908 if ($right
< 1) $right
= 1;
7909 if ($right
> this.options
.maxPage
- 1) $right
= this.options
.maxPage
- 1;
7911 if ($linksBefore
>= $half
) {
7915 $left
-= $linksBefore
;
7916 $right
+= $half
- $linksBefore
;
7919 if ($linksAfter
>= $half
) {
7923 $right
+= $linksAfter
;
7924 $left
-= $half
- $linksAfter
;
7927 $right
= Math
.ceil($right
);
7928 $left
= Math
.ceil($left
);
7929 if ($left
< 1) $left
= 1;
7930 if ($right
> this.options
.maxPage
) $right
= this.options
.maxPage
;
7934 if ($left
- 1 < 2) {
7935 $pageList
.append(this._renderLink(2));
7938 $('<li class="button jumpTo"><a title="' + WCF
.Language
.get('wcf.global.page.jumpTo') + '" class="jsTooltip">…</a></li>').appendTo($pageList
);
7939 $hasHiddenPages
= true;
7944 for (var $i
= $left
+ 1; $i
< $right
; $i
++) {
7945 $pageList
.append(this._renderLink($i
));
7949 if ($right
< this.options
.maxPage
) {
7950 if (this.options
.maxPage
- $right
< 2) {
7951 $pageList
.append(this._renderLink(this.options
.maxPage
- 1));
7954 $('<li class="button jumpTo"><a title="' + WCF
.Language
.get('wcf.global.page.jumpTo') + '" class="jsTooltip">…</a></li>').appendTo($pageList
);
7955 $hasHiddenPages
= true;
7960 $pageList
.append(this._renderLink(this.options
.maxPage
));
7963 var $nextElement
= $('<li></li>').addClass('button skip');
7964 $pageList
.append($nextElement
);
7966 if (this.options
.activePage
< this.options
.maxPage
) {
7967 var $nextLink
= $('<a' + ((this.options
.nextPage
!= null) ? (' title="' + this.options
.nextPage
+ '"') : ('')) + '></a>');
7968 $nextElement
.append($nextLink
);
7969 this._bindSwitchPage($nextLink
, this.options
.activePage
+ 1);
7971 var $nextImage
= $('<img src="' + this.options
.nextIcon
+ '" alt="" />');
7972 $nextLink
.append($nextImage
);
7975 var $nextImage
= $('<img src="' + this.options
.nextIcon
+ '" alt="" />');
7976 $nextElement
.append($nextImage
);
7977 $nextElement
.addClass('disabled');
7978 $nextImage
.addClass('disabled');
7980 $nextImage
.addClass('icon16');
7982 if ($hasHiddenPages
) {
7983 $pageList
.data('pages', this.options
.maxPage
);
7984 WCF
.System
.PageNavigation
.init('#' + $pageList
.wcfIdentify(), $.proxy(function(pageNo
) {
7985 this.switchPage(pageNo
);
7990 // otherwise hide the paginator if not already hidden
7991 this.element
.hide();
7996 * Renders a page link.
7998 * @parameter integer page
8001 _renderLink: function(page
, lineBreak
) {
8002 var $pageElement
= $('<li class="button"></li>');
8003 if (lineBreak
!= undefined && lineBreak
) {
8004 $pageElement
.addClass('break');
8006 if (page
!= this.options
.activePage
) {
8007 var $pageLink
= $('<a>' + WCF
.String
.addThousandsSeparator(page
) + '</a>');
8008 $pageElement
.append($pageLink
);
8009 this._bindSwitchPage($pageLink
, page
);
8012 $pageElement
.addClass('active');
8013 var $pageSubElement
= $('<span>' + WCF
.String
.addThousandsSeparator(page
) + '</span>');
8014 $pageElement
.append($pageSubElement
);
8017 return $pageElement
;
8021 * Binds the 'click'-event for the page switching to the given element.
8023 * @parameter $(element) element
8024 * @paremeter integer page
8026 _bindSwitchPage: function(element
, page
) {
8028 element
.click(function() {
8029 $self
.switchPage(page
);
8034 * Switches to the given page
8036 * @parameter Event event
8037 * @parameter integer page
8039 switchPage: function(page
) {
8040 this._setOption('activePage', page
);
8044 * Sets the given option to the given value.
8045 * See the jQuery UI widget documentation for more.
8047 _setOption: function(key
, value
) {
8048 if (key
== 'activePage') {
8049 if (value
!= this.options
[key
] && value
> 0 && value
<= this.options
.maxPage
) {
8050 // you can prevent the page switching by returning false or by event.preventDefault()
8051 // in a shouldSwitch-callback. e.g. if an AJAX request is already running.
8052 var $result
= this._trigger('shouldSwitch', undefined, {
8056 if ($result
|| $result
!== undefined) {
8057 this.options
[key
] = value
;
8059 this._trigger('switched', undefined, {
8064 this._trigger('notSwitched', undefined, {
8071 this.options
[key
] = value
;
8073 if (key
== 'disabled') {
8075 this.element
.children().remove();
8081 else if (key
== 'maxPage') {
8090 * Start input of pagenumber
8092 * @parameter Event event
8094 _startInput: function(event
) {
8096 var $childLink
= $(event
.currentTarget
);
8097 if (!$childLink
.is('a')) $childLink
= $childLink
.parent('a');
8102 var $childInput
= $childLink
.parent('li').children('input')
8103 .css('display', 'block')
8106 $childInput
.focus();
8110 * Stops input of pagenumber
8112 * @parameter Event event
8114 _stopInput: function(event
) {
8116 var $childInput
= $(event
.currentTarget
);
8117 $childInput
.css('display', 'none');
8120 var $childContainer
= $childInput
.parent('li');
8121 if ($childContainer
!= undefined && $childContainer
!= null) {
8122 $childContainer
.children('a').show();
8127 * Handles input of pagenumber
8129 * @parameter Event event
8131 _handleInput: function(event
) {
8132 var $ie7
= ($.browser
.msie
&& $.browser
.version
== '7.0');
8133 if (event
.type
!= 'keyup' || $ie7
) {
8134 if (!$ie7
|| ((event
.which
== 13 || event
.which
== 27) && event
.type
== 'keyup')) {
8135 if (event
.which
== 13) {
8136 this.switchPage(parseInt($(event
.currentTarget
).val()));
8139 if (event
.which
== 13 || event
.which
== 27) {
8140 this._stopInput(event
);
8141 event
.stopPropagation();
8149 * Encapsulate eval() within an own function to prevent problems
8150 * with optimizing and minifiny JS.
8152 * @param mixed expression
8155 function wcfEval(expression
) {
8156 return eval(expression
);