Added first day of the week setting
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WCF.js
CommitLineData
158bd3ca 1/**
e919d1d3 2 * Class and function collection for WCF.
158bd3ca 3 *
95d8093a
AE
4 * Major Contributors: Markus Bartz, Tim Duesterhus, Matthias Schmidt and Marcel Werk
5 *
6 * @author Alexander Ebert
e919d1d3 7 * @copyright 2001-2013 WoltLab GmbH
158bd3ca
TD
8 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
9 */
10
e93829d0
AE
11(function() {
12 // store original implementation
13 var $jQueryData = jQuery.fn.data;
9f959ced 14
e93829d0
AE
15 /**
16 * Override jQuery.fn.data() to support custom 'ID' suffix which will
17 * be translated to '-id' at runtime.
18 *
19 * @see jQuery.fn.data()
20 */
21 jQuery.fn.data = function(key, value) {
1f4f33dd
AE
22 if (key) {
23 switch (typeof key) {
24 case 'object':
25 for (var $key in key) {
26 if ($key.match(/ID$/)) {
27 var $value = key[$key];
28 delete key[$key];
29
30 $key = $key.replace(/ID$/, '-id');
31 key[$key] = $value;
32 }
33 }
34
35 arguments[0] = key;
36 break;
37
38 case 'string':
39 if (key.match(/ID$/)) {
40 arguments[0] = key.replace(/ID$/, '-id');
41 }
42 break;
43 }
e93829d0 44 }
9f959ced 45
e93829d0 46 // call jQuery's own data method
1c93e6e7
AE
47 var $data = $jQueryData.apply(this, arguments);
48
49 // handle .data() call without arguments
50 if (key === undefined) {
51 for (var $key in $data) {
52 if ($key.match(/Id$/)) {
696930b9 53 $data[$key.replace(/Id$/, 'ID')] = $data[$key];
1c93e6e7
AE
54 delete $data[$key];
55 }
56 }
57 }
58
59 return $data;
f4126129 60 };
e919d1d3 61
0af61800
TD
62 // provide a sane window.console implementation
63 if (!window.console) window.console = { };
64 var consoleProperties = [ "log",/* "debug",*/ "info", "warn", "exception", "assert", "dir", "dirxml", "trace", "group", "groupEnd", "groupCollapsed", "profile", "profileEnd", "count", "clear", "time", "timeEnd", "timeStamp", "table", "error" ];
65 for (var i = 0; i < consoleProperties.length; i++) {
66 if (typeof (console[consoleProperties[i]]) === 'undefined') {
89be55b6 67 console[consoleProperties[i]] = function () { };
0af61800 68 }
e919d1d3 69 }
0af61800
TD
70
71 if (typeof(console.debug) === 'undefined') {
e919d1d3
AE
72 // forward console.debug to console.log (IE9)
73 console.debug = function(string) { console.log(string); };
74 }
e93829d0
AE
75})();
76
9f959ced
MS
77/**
78 * Simple JavaScript Inheritance
878d0d80
AE
79 * By John Resig http://ejohn.org/
80 * MIT Licensed.
81 */
82// Inspired by base2 and Prototype
f4126129 83(function(){var a=false,b=/xyz/.test(function(){xyz})?/\b_super\b/:/.*/;this.Class=function(){};Class.extend=function(c){function g(){if(!a&&this.init)this.init.apply(this,arguments);}var d=this.prototype;a=true;var e=new this;a=false;for(var f in c){e[f]=typeof c[f]=="function"&&typeof d[f]=="function"&&b.test(c[f])?function(a,b){return function(){var c=this._super;this._super=d[a];var e=b.apply(this,arguments);this._super=c;return e;};}(f,c[f]):c[f]}g.prototype=e;g.prototype.constructor=g;g.extend=arguments.callee;return g;};})();
878d0d80 84
f0ef56c5
AE
85/*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas, David Knight. Dual MIT/BSD license */
86window.matchMedia||(window.matchMedia=function(){"use strict";var e=window.styleMedia||window.media;if(!e){var t=document.createElement("style"),n=document.getElementsByTagName("script")[0],r=null;t.type="text/css";t.id="matchmediajs-test";n.parentNode.insertBefore(t,n);r="getComputedStyle"in window&&window.getComputedStyle(t,null)||t.currentStyle;e={matchMedium:function(e){var n="@media "+e+"{ #matchmediajs-test { width: 1px; } }";if(t.styleSheet){t.styleSheet.cssText=n}else{t.textContent=n}return r.width==="1px"}}}return function(t){return{matches:e.matchMedium(t||"all"),media:t||"all"}}}());
87
88/*! matchMedia() polyfill addListener/removeListener extension. Author & copyright (c) 2012: Scott Jehl. Dual MIT/BSD license */
89(function(){if(window.matchMedia&&window.matchMedia("all").addListener){return false}var e=window.matchMedia,t=e("only all").matches,n=false,r=0,i=[],s=function(t){clearTimeout(r);r=setTimeout(function(){for(var t=0,n=i.length;t<n;t++){var r=i[t].mql,s=i[t].listeners||[],o=e(r.media).matches;if(o!==r.matches){r.matches=o;for(var u=0,a=s.length;u<a;u++){s[u].call(window,r)}}}},30)};window.matchMedia=function(r){var o=e(r),u=[],a=0;o.addListener=function(e){if(!t){return}if(!n){n=true;window.addEventListener("resize",s,true)}if(a===0){a=i.push({mql:o,listeners:u})}u.push(e)};o.removeListener=function(e){for(var t=0,n=u.length;t<n;t++){if(u[t]===e){u.splice(t,1)}}};return o}})();
90
1a65be7c
AE
91/*!
92 * enquire.js v2.1.0 - Awesome Media Queries in JavaScript
93 * Copyright (c) 2013 Nick Williams - http://wicky.nillia.ms/enquire.js
94 * License: MIT (http://www.opensource.org/licenses/mit-license.php)
95 */
47516fcd 96(function(t,i,n){var e=i.matchMedia;"undefined"!=typeof module&&module.exports?module.exports=n(e):"function"==typeof define&&define.amd?define(function(){return i[t]=n(e)}):i[t]=n(e)})("enquire",this,function(t){"use strict";function i(t,i){var n,e=0,s=t.length;for(e;s>e&&(n=i(t[e],e),n!==!1);e++);}function n(t){return"[object Array]"===Object.prototype.toString.apply(t)}function e(t){return"function"==typeof t}function s(t){this.options=t,!t.deferSetup&&this.setup()}function o(i,n){this.query=i,this.isUnconditional=n,this.handlers=[],this.mql=t(i);var e=this;this.listener=function(t){e.mql=t,e.assess()},this.mql.addListener(this.listener)}function r(){if(!t)throw Error("matchMedia not present, legacy browsers require a polyfill");this.queries={},this.browserIsIncapable=!t("only all").matches}return s.prototype={setup:function(){this.options.setup&&this.options.setup(),this.initialised=!0},on:function(){!this.initialised&&this.setup(),this.options.match&&this.options.match()},off:function(){this.options.unmatch&&this.options.unmatch()},destroy:function(){this.options.destroy?this.options.destroy():this.off()},equals:function(t){return this.options===t||this.options.match===t}},o.prototype={addHandler:function(t){var i=new s(t);this.handlers.push(i),this.matches()&&i.on()},removeHandler:function(t){var n=this.handlers;i(n,function(i,e){return i.equals(t)?(i.destroy(),!n.splice(e,1)):void 0})},matches:function(){return this.mql.matches||this.isUnconditional},clear:function(){i(this.handlers,function(t){t.destroy()}),this.mql.removeListener(this.listener),this.handlers.length=0},assess:function(){var t=this.matches()?"on":"off";i(this.handlers,function(i){i[t]()})}},r.prototype={register:function(t,s,r){var h=this.queries,u=r&&this.browserIsIncapable;return h[t]||(h[t]=new o(t,u)),e(s)&&(s={match:s}),n(s)||(s=[s]),i(s,function(i){h[t].addHandler(i)}),this},unregister:function(t,i){var n=this.queries[t];return n&&(i?n.removeHandler(i):(n.clear(),delete this.queries[t])),this}},new r});
1a65be7c 97
efce8a94
AE
98/*! head.load - v1.0.3 */
99(function(n,t){"use strict";function w(){}function u(n,t){if(n){typeof n=="object"&&(n=[].slice.call(n));for(var i=0,r=n.length;i<r;i++)t.call(n,n[i],i)}}function it(n,i){var r=Object.prototype.toString.call(i).slice(8,-1);return i!==t&&i!==null&&r===n}function s(n){return it("Function",n)}function a(n){return it("Array",n)}function et(n){var i=n.split("/"),t=i[i.length-1],r=t.indexOf("?");return r!==-1?t.substring(0,r):t}function f(n){(n=n||w,n._done)||(n(),n._done=1)}function ot(n,t,r,u){var f=typeof n=="object"?n:{test:n,success:!t?!1:a(t)?t:[t],failure:!r?!1:a(r)?r:[r],callback:u||w},e=!!f.test;return e&&!!f.success?(f.success.push(f.callback),i.load.apply(null,f.success)):e||!f.failure?u():(f.failure.push(f.callback),i.load.apply(null,f.failure)),i}function v(n){var t={},i,r;if(typeof n=="object")for(i in n)!n[i]||(t={name:i,url:n[i]});else t={name:et(n),url:n};return(r=c[t.name],r&&r.url===t.url)?r:(c[t.name]=t,t)}function y(n){n=n||c;for(var t in n)if(n.hasOwnProperty(t)&&n[t].state!==l)return!1;return!0}function st(n){n.state=ft;u(n.onpreload,function(n){n.call()})}function ht(n){n.state===t&&(n.state=nt,n.onpreload=[],rt({url:n.url,type:"cache"},function(){st(n)}))}function ct(){var n=arguments,t=n[n.length-1],r=[].slice.call(n,1),f=r[0];return(s(t)||(t=null),a(n[0]))?(n[0].push(t),i.load.apply(null,n[0]),i):(f?(u(r,function(n){s(n)||!n||ht(v(n))}),b(v(n[0]),s(f)?f:function(){i.load.apply(null,r)})):b(v(n[0])),i)}function lt(){var n=arguments,t=n[n.length-1],r={};return(s(t)||(t=null),a(n[0]))?(n[0].push(t),i.load.apply(null,n[0]),i):(u(n,function(n){n!==t&&(n=v(n),r[n.name]=n)}),u(n,function(n){n!==t&&(n=v(n),b(n,function(){y(r)&&f(t)}))}),i)}function b(n,t){if(t=t||w,n.state===l){t();return}if(n.state===tt){i.ready(n.name,t);return}if(n.state===nt){n.onpreload.push(function(){b(n,t)});return}n.state=tt;rt(n,function(){n.state=l;t();u(h[n.name],function(n){f(n)});o&&y()&&u(h.ALL,function(n){f(n)})})}function at(n){n=n||"";var t=n.split("?")[0].split(".");return t[t.length-1].toLowerCase()}function rt(t,i){function e(t){t=t||n.event;u.onload=u.onreadystatechange=u.onerror=null;i()}function o(f){f=f||n.event;(f.type==="load"||/loaded|complete/.test(u.readyState)&&(!r.documentMode||r.documentMode<9))&&(n.clearTimeout(t.errorTimeout),n.clearTimeout(t.cssTimeout),u.onload=u.onreadystatechange=u.onerror=null,i())}function s(){if(t.state!==l&&t.cssRetries<=20){for(var i=0,f=r.styleSheets.length;i<f;i++)if(r.styleSheets[i].href===u.href){o({type:"load"});return}t.cssRetries++;t.cssTimeout=n.setTimeout(s,250)}}var u,h,f;i=i||w;h=at(t.url);h==="css"?(u=r.createElement("link"),u.type="text/"+(t.type||"css"),u.rel="stylesheet",u.href=t.url,t.cssRetries=0,t.cssTimeout=n.setTimeout(s,500)):(u=r.createElement("script"),u.type="text/"+(t.type||"javascript"),u.src=t.url);u.onload=u.onreadystatechange=o;u.onerror=e;u.async=!1;u.defer=!1;t.errorTimeout=n.setTimeout(function(){e({type:"timeout"})},7e3);f=r.head||r.getElementsByTagName("head")[0];f.insertBefore(u,f.lastChild)}function vt(){for(var t,u=r.getElementsByTagName("script"),n=0,f=u.length;n<f;n++)if(t=u[n].getAttribute("data-headjs-load"),!!t){i.load(t);return}}function yt(n,t){var v,p,e;return n===r?(o?f(t):d.push(t),i):(s(n)&&(t=n,n="ALL"),a(n))?(v={},u(n,function(n){v[n]=c[n];i.ready(n,function(){y(v)&&f(t)})}),i):typeof n!="string"||!s(t)?i:(p=c[n],p&&p.state===l||n==="ALL"&&y()&&o)?(f(t),i):(e=h[n],e?e.push(t):e=h[n]=[t],i)}function e(){if(!r.body){n.clearTimeout(i.readyTimeout);i.readyTimeout=n.setTimeout(e,50);return}o||(o=!0,vt(),u(d,function(n){f(n)}))}function k(){r.addEventListener?(r.removeEventListener("DOMContentLoaded",k,!1),e()):r.readyState==="complete"&&(r.detachEvent("onreadystatechange",k),e())}var r=n.document,d=[],h={},c={},ut="async"in r.createElement("script")||"MozAppearance"in r.documentElement.style||n.opera,o,g=n.head_conf&&n.head_conf.head||"head",i=n[g]=n[g]||function(){i.ready.apply(null,arguments)},nt=1,ft=2,tt=3,l=4,p;if(r.readyState==="complete")e();else if(r.addEventListener)r.addEventListener("DOMContentLoaded",k,!1),n.addEventListener("load",e,!1);else{r.attachEvent("onreadystatechange",k);n.attachEvent("onload",e);p=!1;try{p=!n.frameElement&&r.documentElement}catch(wt){}p&&p.doScroll&&function pt(){if(!o){try{p.doScroll("left")}catch(t){n.clearTimeout(i.readyTimeout);i.readyTimeout=n.setTimeout(pt,50);return}e()}}()}i.load=i.js=ut?lt:ct;i.test=ot;i.ready=yt;i.ready(r,function(){y()&&u(h.ALL,function(n){f(n)});i.feature&&i.feature("domloaded",!0)})})(window);
100/*
eb1537e3 101//# sourceMappingURL=head.load.min.js.map
efce8a94 102*/
eb1537e3 103
0937237b
AE
104/**
105 * Provides a hashCode() method for strings, similar to Java's String.hashCode().
106 *
107 * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
108 */
109String.prototype.hashCode = function() {
110 var $char;
111 var $hash = 0;
112
113 if (this.length) {
114 for (var $i = 0, $length = this.length; $i < $length; $i++) {
115 $char = this.charCodeAt($i);
116 $hash = (($hash << 5) - $hash) + $char;
117 $hash = $hash & $hash; // convert to 32bit integer
118 }
119 }
120
121 return $hash;
7382b20f 122};
0937237b 123
b108eaa5 124/**
eb69a2e9
AE
125 * User-Agent based browser detection and touch detection.
126 */
49d0d797 127(function() {
eb69a2e9
AE
128 var ua = navigator.userAgent.toLowerCase();
129 var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
130 /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
131 /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
132 /(msie) ([\w.]+)/.exec( ua ) ||
133 ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
134 [];
135
136 var matched = {
137 browser: match[ 1 ] || "",
138 version: match[ 2 ] || "0"
139 };
140 browser = {};
141
142 if ( matched.browser ) {
143 browser[ matched.browser ] = true;
144 browser.version = matched.version;
145 }
146
147 // Chrome is Webkit, but Webkit is also Safari.
148 if ( browser.chrome ) {
149 browser.webkit = true;
150 } else if ( browser.webkit ) {
151 browser.safari = true;
152 }
153
154 jQuery.browser = browser;
555561be 155 jQuery.browser.touch = (!!('ontouchstart' in window) || (!!('msMaxTouchPoints' in window.navigator) && window.navigator.msMaxTouchPoints > 0));
9899046c
AE
156
157 // detect smartphones
f7461bbb 158 jQuery.browser.smartphone = ($('html').css('caption-side') == 'bottom');
e3f96cf0
AE
159
160 // CKEditor support (disabled for Android & Windows Phone)
161 jQuery.browser.ckeditor = (navigator.userAgent.match(/(Android|Windows Phone)/i)) ? false : true;
f52b6fc6
AE
162
163 // properly detect IE11
164 if (jQuery.browser.mozilla && ua.match(/trident/)) {
165 jQuery.browser.mozilla = false;
166 jQuery.browser.msie = true;
167 }
49d0d797
MW
168})();
169
3f42eecb 170/**
43392e40 171 * jQuery.browser.mobile (http://detectmobilebrowser.com/)
60c25707 172 *
43392e40 173 * jQuery.browser.mobile will be true if the browser is a mobile device
60c25707 174 *
43392e40 175 **/
74810514 176(function(a){(jQuery.browser=jQuery.browser||{}).mobile=/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))})(navigator.userAgent||navigator.vendor||window.opera);
3f42eecb 177
158bd3ca
TD
178/**
179 * Initialize WCF namespace
180 */
181var WCF = {};
182
183/**
184 * Extends jQuery with additional methods.
185 */
186$.extend(true, {
a009a689 187 /**
acf25f0f 188 * Removes the given value from the given array and returns the array.
a009a689
MS
189 *
190 * @param array array
191 * @param mixed element
192 * @return array
193 */
194 removeArrayValue: function(array, value) {
195 return $.grep(array, function(element, index) {
196 return value !== element;
197 });
198 },
199
158bd3ca
TD
200 /**
201 * Escapes an ID to work with jQuery selectors.
7b608580 202 *
158bd3ca
TD
203 * @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
204 * @param string id
205 * @return string
206 */
207 wcfEscapeID: function(id) {
208 return id.replace(/(:|\.)/g, '\\$1');
209 },
210
211 /**
212 * Returns true if given ID exists within DOM.
213 *
214 * @param string id
215 * @return boolean
216 */
217 wcfIsset: function(id) {
218 return !!$('#' + $.wcfEscapeID(id)).length;
2b4d2743 219 },
9f959ced 220
2b4d2743
AE
221 /**
222 * Returns the length of an object.
223 *
224 * @param object targetObject
225 * @return integer
226 */
227 getLength: function(targetObject) {
228 var $length = 0;
9f959ced 229
2b4d2743
AE
230 for (var $key in targetObject) {
231 if (targetObject.hasOwnProperty($key)) {
232 $length++;
233 }
234 }
7b608580 235
2b4d2743 236 return $length;
158bd3ca
TD
237 }
238});
239
240/**
241 * Extends jQuery's chainable methods.
242 */
243$.fn.extend({
244 /**
404c9abe 245 * Returns tag name of first jQuery element.
158bd3ca
TD
246 *
247 * @returns string
248 */
249 getTagName: function() {
d487dd36 250 return (this.length) ? this.get(0).tagName.toLowerCase() : '';
158bd3ca
TD
251 },
252
253 /**
254 * Returns the dimensions for current element.
255 *
256 * @see http://api.jquery.com/hidden-selector/
257 * @param string type
258 * @return object
259 */
260 getDimensions: function(type) {
261 var dimensions = css = {};
262 var wasHidden = false;
263
264 // show element to retrieve dimensions and restore them later
265 if (this.is(':hidden')) {
69057ad5 266 css = WCF.getInlineCSS(this);
158bd3ca
TD
267
268 wasHidden = true;
269
270 this.css({
271 display: 'block',
272 visibility: 'hidden'
273 });
274 }
275
276 switch (type) {
277 case 'inner':
278 dimensions = {
279 height: this.innerHeight(),
280 width: this.innerWidth()
281 };
282 break;
283
284 case 'outer':
285 dimensions = {
286 height: this.outerHeight(),
287 width: this.outerWidth()
288 };
289 break;
290
291 default:
292 dimensions = {
293 height: this.height(),
294 width: this.width()
295 };
296 break;
297 }
298
299 // restore previous settings
300 if (wasHidden) {
69057ad5 301 WCF.revertInlineCSS(this, css, [ 'display', 'visibility' ]);
158bd3ca
TD
302 }
303
304 return dimensions;
305 },
306
307 /**
308 * Returns the offsets for current element, defaults to position
309 * relative to document.
310 *
311 * @see http://api.jquery.com/hidden-selector/
312 * @param string type
313 * @return object
314 */
315 getOffsets: function(type) {
316 var offsets = css = {};
317 var wasHidden = false;
318
319 // show element to retrieve dimensions and restore them later
320 if (this.is(':hidden')) {
69057ad5 321 css = WCF.getInlineCSS(this);
158bd3ca
TD
322 wasHidden = true;
323
324 this.css({
325 display: 'block',
326 visibility: 'hidden'
327 });
328 }
329
330 switch (type) {
331 case 'offset':
332 offsets = this.offset();
333 break;
334
335 case 'position':
336 default:
337 offsets = this.position();
338 break;
339 }
340
341 // restore previous settings
342 if (wasHidden) {
69057ad5 343 WCF.revertInlineCSS(this, css, [ 'display', 'visibility' ]);
158bd3ca
TD
344 }
345
346 return offsets;
347 },
348
349 /**
350 * Changes element's position to 'absolute' or 'fixed' while maintaining it's
351 * current position relative to viewport. Optionally removes element from
352 * current DOM-node and moving it into body-element (useful for drag & drop)
353 *
354 * @param boolean rebase
355 * @return object
356 */
357 makePositioned: function(position, rebase) {
358 if (position != 'absolute' && position != 'fixed') {
359 position = 'absolute';
360 }
361
362 var $currentPosition = this.getOffsets('position');
363 this.css({
364 position: position,
365 left: $currentPosition.left,
366 margin: 0,
367 top: $currentPosition.top
368 });
369
370 if (rebase) {
371 this.remove().appentTo('body');
372 }
373
374 return this;
375 },
376
377 /**
378 * Disables a form element.
379 *
06355ec3 380 * @return jQuery
158bd3ca
TD
381 */
382 disable: function() {
383 return this.attr('disabled', 'disabled');
384 },
385
386 /**
387 * Enables a form element.
388 *
389 * @return jQuery
390 */
391 enable: function() {
392 return this.removeAttr('disabled');
393 },
9f959ced 394
b3991cb3
AE
395 /**
396 * Returns the element's id. If none is set, a random unique
397 * ID will be assigned.
398 *
399 * @return string
400 */
401 wcfIdentify: function() {
402 if (!this.attr('id')) {
403 this.attr('id', WCF.getRandomID());
404 }
9f959ced 405
b3991cb3
AE
406 return this.attr('id');
407 },
158bd3ca 408
0bfde690
AE
409 /**
410 * Returns the caret position of current element. If the element
411 * does not equal input[type=text], input[type=password] or
412 * textarea, -1 is returned.
413 *
414 * @return integer
415 */
416 getCaret: function() {
404c9abe 417 if (this.is('input')) {
0bfde690
AE
418 if (this.attr('type') != 'text' && this.attr('type') != 'password') {
419 return -1;
420 }
421 }
404c9abe 422 else if (!this.is('textarea')) {
0bfde690
AE
423 return -1;
424 }
425
426 var $position = 0;
427 var $element = this.get(0);
428 if (document.selection) { // IE 8
429 // set focus to enable caret on this element
430 this.focus();
431
432 var $selection = document.selection.createRange();
433 $selection.moveStart('character', -this.val().length);
434 $position = $selection.text.length;
435 }
436 else if ($element.selectionStart || $element.selectionStart == '0') { // Opera, Chrome, Firefox, Safari, IE 9+
437 $position = parseInt($element.selectionStart);
438 }
439
440 return $position;
441 },
442
f73ff47d
TD
443 /**
444 * Sets the caret position of current element. If the element
445 * does not equal input[type=text], input[type=password] or
446 * textarea, false is returned.
447 *
448 * @param integer position
449 * @return boolean
450 */
451 setCaret: function (position) {
404c9abe 452 if (this.is('input')) {
f73ff47d
TD
453 if (this.attr('type') != 'text' && this.attr('type') != 'password') {
454 return false;
455 }
456 }
404c9abe 457 else if (!this.is('textarea')) {
f73ff47d
TD
458 return false;
459 }
460
461 var $element = this.get(0);
462
463 // set focus to enable caret on this element
464 this.focus();
465 if (document.selection) { // IE 8
466 var $selection = document.selection.createRange();
467 $selection.moveStart('character', position);
468 $selection.moveEnd('character', 0);
469 $selection.select();
470 }
471 else if ($element.selectionStart || $element.selectionStart == '0') { // Opera, Chrome, Firefox, Safari, IE 9+
472 $element.selectionStart = position;
473 $element.selectionEnd = position;
474 }
475
476 return true;
477 },
478
158bd3ca
TD
479 /**
480 * Shows an element by sliding and fading it into viewport.
481 *
482 * @param string direction
483 * @param object callback
453aced6 484 * @param integer duration
158bd3ca
TD
485 * @returns jQuery
486 */
453aced6 487 wcfDropIn: function(direction, callback, duration) {
158bd3ca 488 if (!direction) direction = 'up';
453aced6 489 if (!duration || !parseInt(duration)) duration = 200;
158bd3ca 490
404c9abe 491 return this.show(WCF.getEffect(this, 'drop'), { direction: direction }, duration, callback);
158bd3ca
TD
492 },
493
494 /**
495 * Hides an element by sliding and fading it out the viewport.
496 *
497 * @param string direction
498 * @param object callback
453aced6 499 * @param integer duration
158bd3ca
TD
500 * @returns jQuery
501 */
453aced6 502 wcfDropOut: function(direction, callback, duration) {
158bd3ca 503 if (!direction) direction = 'down';
453aced6 504 if (!duration || !parseInt(duration)) duration = 200;
158bd3ca 505
404c9abe 506 return this.hide(WCF.getEffect(this, 'drop'), { direction: direction }, duration, callback);
158bd3ca
TD
507 },
508
509 /**
510 * Shows an element by blinding it up.
511 *
512 * @param string direction
513 * @param object callback
78e4d558 514 * @param integer duration
158bd3ca
TD
515 * @returns jQuery
516 */
78e4d558 517 wcfBlindIn: function(direction, callback, duration) {
158bd3ca 518 if (!direction) direction = 'vertical';
78e4d558 519 if (!duration || !parseInt(duration)) duration = 200;
158bd3ca 520
404c9abe 521 return this.show(WCF.getEffect(this, 'blind'), { direction: direction }, duration, callback);
158bd3ca
TD
522 },
523
524 /**
525 * Hides an element by blinding it down.
526 *
527 * @param string direction
528 * @param object callback
78e4d558 529 * @param integer duration
158bd3ca
TD
530 * @returns jQuery
531 */
78e4d558 532 wcfBlindOut: function(direction, callback, duration) {
158bd3ca 533 if (!direction) direction = 'vertical';
78e4d558 534 if (!duration || !parseInt(duration)) duration = 200;
158bd3ca 535
404c9abe 536 return this.hide(WCF.getEffect(this, 'blind'), { direction: direction }, duration, callback);
158bd3ca
TD
537 },
538
539 /**
540 * Highlights an element.
541 *
542 * @param object options
543 * @param object callback
544 * @returns jQuery
545 */
546 wcfHighlight: function(options, callback) {
547 return this.effect('highlight', options, 600, callback);
25763f41 548 },
9f959ced 549
25763f41
AE
550 /**
551 * Shows an element by fading it in.
552 *
553 * @param object callback
554 * @param integer duration
555 * @returns jQuery
556 */
557 wcfFadeIn: function(callback, duration) {
558 if (!duration || !parseInt(duration)) duration = 200;
559
404c9abe 560 return this.show(WCF.getEffect(this, 'fade'), { }, duration, callback);
25763f41 561 },
9f959ced 562
25763f41
AE
563 /**
564 * Hides an element by fading it out.
565 *
566 * @param object callback
567 * @param integer duration
568 * @returns jQuery
569 */
570 wcfFadeOut: function(callback, duration) {
571 if (!duration || !parseInt(duration)) duration = 200;
572
404c9abe 573 return this.hide(WCF.getEffect(this, 'fade'), { }, duration, callback);
158bd3ca
TD
574 }
575});
576
577/**
578 * WoltLab Community Framework core methods
579 */
580$.extend(WCF, {
5a1b4042
AE
581 /**
582 * count of active dialogs
583 * @var integer
584 */
585 activeDialogs: 0,
586
158bd3ca 587 /**
4da866ad 588 * Counter for dynamic element ids
7b608580 589 *
158bd3ca
TD
590 * @var integer
591 */
592 _idCounter: 0,
9f959ced 593
158bd3ca
TD
594 /**
595 * Returns a dynamically created id.
596 *
a9e6c5a7 597 * @see https://github.com/sstephenson/prototype/blob/5e5cfff7c2c253eaf415c279f9083b4650cd4506/src/prototype/dom/dom.js#L1789
158bd3ca
TD
598 * @return string
599 */
600 getRandomID: function() {
601 var $elementID = '';
602
603 do {
604 $elementID = 'wcf' + this._idCounter++;
605 }
606 while ($.wcfIsset($elementID));
607
608 return $elementID;
609 },
610
611 /**
612 * Wrapper for $.inArray which returns boolean value instead of
613 * index value, similar to PHP's in_array().
614 *
615 * @param mixed needle
616 * @param array haystack
617 * @return boolean
618 */
619 inArray: function(needle, haystack) {
620 return ($.inArray(needle, haystack) != -1);
621 },
622
623 /**
624 * Adjusts effect for partially supported elements.
625 *
404c9abe 626 * @param jQuery object
158bd3ca
TD
627 * @param string effect
628 * @return string
629 */
404c9abe 630 getEffect: function(object, effect) {
158bd3ca 631 // most effects are not properly supported on table rows, use highlight instead
404c9abe 632 if (object.is('tr')) {
158bd3ca
TD
633 return 'highlight';
634 }
635
636 return effect;
69057ad5
AE
637 },
638
639 /**
640 * Returns inline CSS for given element.
641 *
642 * @param jQuery element
643 * @return object
644 */
645 getInlineCSS: function(element) {
646 var $inlineStyles = { };
647 var $style = element.attr('style');
648
649 // no style tag given or empty
650 if (!$style) {
651 return { };
652 }
653
654 $style = $style.split(';');
655 for (var $i = 0, $length = $style.length; $i < $length; $i++) {
656 var $fragment = $.trim($style[$i]);
657 if ($fragment == '') {
658 continue;
659 }
660
661 $fragment = $fragment.split(':');
662 $inlineStyles[$.trim($fragment[0])] = $.trim($fragment[1]);
663 }
664
665 return $inlineStyles;
666 },
667
668 /**
669 * Reverts inline CSS or negates a previously set property.
670 *
671 * @param jQuery element
672 * @param object inlineCSS
673 * @param array<string> targetProperties
674 */
675 revertInlineCSS: function(element, inlineCSS, targetProperties) {
676 for (var $i = 0, $length = targetProperties.length; $i < $length; $i++) {
677 var $property = targetProperties[$i];
678
679 // revert inline CSS
680 if (inlineCSS[$property]) {
681 element.css($property, inlineCSS[$property]);
682 }
683 else {
684 // negate inline CSS
685 element.css($property, '');
686 }
687 }
158bd3ca
TD
688 }
689});
690
b8b58a7e
AE
691/**
692 * Browser related functions.
693 */
694WCF.Browser = {
695 /**
696 * determines if browser is chrome
697 * @var boolean
698 */
699 _isChrome: null,
700
701 /**
702 * Returns true, if browser is Chrome, Chromium or using GoogleFrame for Internet Explorer.
703 *
704 * @return boolean
705 */
706 isChrome: function() {
707 if (this._isChrome === null) {
708 this._isChrome = false;
709 if (/chrom(e|ium)/.test(navigator.userAgent.toLowerCase())) {
710 this._isChrome = true;
711 }
712 }
713
714 return this._isChrome;
715 }
716};
717
0d6ea23f 718/**
184a8d6d
AE
719 * Dropdown API
720 */
721WCF.Dropdown = {
722 /**
723 * list of callbacks
724 * @var object
725 */
726 _callbacks: { },
727
728 /**
729 * initialization state
730 * @var boolean
731 */
732 _didInit: false,
733
734 /**
735 * list of registered dropdowns
736 * @var object
737 */
738 _dropdowns: { },
739
3ef8dee9
AE
740 /**
741 * container for dropdown menus
742 * @var object
743 */
744 _menuContainer: null,
745
746 /**
747 * list of registered dropdown menus
748 * @var object
749 */
750 _menus: { },
751
184a8d6d
AE
752 /**
753 * Initializes dropdowns.
754 */
755 init: function() {
3ef8dee9
AE
756 if (this._menuContainer === null) {
757 this._menuContainer = $('<div id="dropdownMenuContainer" />').appendTo(document.body);
758 }
759
184a8d6d 760 var self = this;
3ef8dee9
AE
761 $('.dropdownToggle:not(.jsDropdownEnabled)').each(function(index, button) {
762 self.initDropdown($(button), false);
184a8d6d
AE
763 });
764
765 if (!this._didInit) {
766 this._didInit = true;
767
768 WCF.CloseOverlayHandler.addCallback('WCF.Dropdown', $.proxy(this._closeAll, this));
769 WCF.DOMNodeInsertedHandler.addCallback('WCF.Dropdown', $.proxy(this.init, this));
978fdd0a 770 $(document).on('scroll', $.proxy(this._scroll, this));
184a8d6d 771 }
289b4a48
MS
772 },
773
774 /**
775 * Handles dropdown positions in overlays when scrolling in the overlay.
776 *
777 * @param object event
778 */
779 _dialogScroll: function(event) {
780 var $dialogContent = $(event.currentTarget);
781 $dialogContent.find('.dropdown.dropdownOpen').each(function(index, element) {
782 var $dropdown = $(element);
d3fb0283
MS
783 var $dropdownID = $dropdown.wcfIdentify();
784 var $dropdownOffset = $dropdown.offset();
785 var $dialogContentOffset = $dialogContent.offset();
786
787 var $verticalScrollTolerance = $(element).height() / 2;
289b4a48 788
7b608580 789 // check if dropdown toggle is still (partially) visible
d3fb0283
MS
790 if ($dropdownOffset.top + $verticalScrollTolerance <= $dialogContentOffset.top) {
791 // top check
792 WCF.Dropdown.toggleDropdown($dropdownID);
793 }
794 else if ($dropdownOffset.top >= $dialogContentOffset.top + $dialogContent.height()) {
795 // bottom check
796 WCF.Dropdown.toggleDropdown($dropdownID);
797 }
798 else if ($dropdownOffset.left <= $dialogContentOffset.left) {
799 // left check
800 WCF.Dropdown.toggleDropdown($dropdownID);
289b4a48 801 }
d3fb0283
MS
802 else if ($dropdownOffset.left >= $dialogContentOffset.left + $dialogContent.width()) {
803 // right check
804 WCF.Dropdown.toggleDropdown($dropdownID);
289b4a48
MS
805 }
806 else {
807 WCF.Dropdown.setAlignmentByID($dropdown.wcfIdentify());
808 }
809 });
810 },
811
812 /**
813 * Handles dropdown positions in overlays when scrolling in the document.
814 *
815 * @param object event
816 */
817 _scroll: function(event) {
818 for (var $containerID in this._dropdowns) {
819 var $dropdown = this._dropdowns[$containerID];
820 if ($dropdown.data('isOverlayDropdownButton') && $dropdown.hasClass('dropdownOpen')) {
821 this.setAlignmentByID($containerID);
822 }
823 }
184a8d6d
AE
824 },
825
3ef8dee9
AE
826 /**
827 * Initializes a dropdown.
828 *
829 * @param jQuery button
830 * @param boolean isLazyInitialization
831 */
832 initDropdown: function(button, isLazyInitialization) {
833 if (button.hasClass('jsDropdownEnabled') || button.data('target')) {
834 return;
835 }
836
837 var $dropdown = button.parents('.dropdown');
838 if (!$dropdown.length) {
839 // broken dropdown, ignore
7b608580 840 console.debug("[WCF.Dropdown] Invalid dropdown passed, button '" + button.wcfIdentify() + "' does not have a parent with .dropdown, aborting.");
3ef8dee9
AE
841 return;
842 }
843
844 var $dropdownMenu = button.next('.dropdownMenu');
845 if (!$dropdownMenu.length) {
846 // broken dropdown, ignore
7b608580 847 console.debug("[WCF.Dropdown] Invalid dropdown passed, dropdown '" + $dropdown.wcfIdentify() + "' does not have a dropdown menu, aborting.");
3ef8dee9
AE
848 return;
849 }
850
851 $dropdownMenu.detach().appendTo(this._menuContainer);
852 var $containerID = $dropdown.wcfIdentify();
853 if (!this._dropdowns[$containerID]) {
854 button.addClass('jsDropdownEnabled').click($.proxy(this._toggle, this));
855
856 this._dropdowns[$containerID] = $dropdown;
857 this._menus[$containerID] = $dropdownMenu;
858 }
859
860 button.data('target', $containerID);
861
862 if (isLazyInitialization) {
863 button.trigger('click');
864 }
865 },
866
a203335d
MS
867 /**
868 * Removes the dropdown with the given container id.
869 *
870 * @param string containerID
871 */
872 removeDropdown: function(containerID) {
873 if (this._menus[containerID]) {
874 $(this._menus[containerID]).remove();
875 delete this._menus[containerID];
876 delete this._dropdowns[containerID];
877 }
878 },
879
38d131ce
AE
880 /**
881 * Initializes a dropdown fragment which behaves like a usual dropdown
882 * but is not controlled by a trigger element.
883 *
884 * @param jQuery dropdown
885 * @param jQuery dropdownMenu
886 */
887 initDropdownFragment: function(dropdown, dropdownMenu) {
888 var $containerID = dropdown.wcfIdentify();
889 if (this._dropdowns[$containerID]) {
890 console.debug("[WCF.Dropdown] Cannot register dropdown identified by '" + $containerID + "' as a fragement.");
891 return;
892 }
893
894 this._dropdowns[$containerID] = dropdown;
895 this._menus[$containerID] = dropdownMenu.detach().appendTo(this._menuContainer);
896 },
897
184a8d6d
AE
898 /**
899 * Registers a callback notified upon dropdown state change.
900 *
901 * @param string identifier
902 * @var object callback
903 */
904 registerCallback: function(identifier, callback) {
905 if (!$.isFunction(callback)) {
906 console.debug("[WCF.Dropdown] Callback for '" + identifier + "' is invalid");
907 return false;
908 }
909
910 if (!this._callbacks[identifier]) {
911 this._callbacks[identifier] = [ ];
912 }
913
914 this._callbacks[identifier].push(callback);
915 },
916
917 /**
918 * Toggles a dropdown.
919 *
920 * @param object event
3ef8dee9 921 * @param string targetID
184a8d6d 922 */
3ef8dee9
AE
923 _toggle: function(event, targetID) {
924 var $targetID = (event === null) ? targetID : $(event.currentTarget).data('target');
2b8f0792 925
289b4a48
MS
926 // check if 'isOverlayDropdownButton' is set which indicates if
927 // the dropdown toggle is in an overlay
928 var $target = this._dropdowns[$targetID];
929 if ($target && $target.data('isOverlayDropdownButton') === undefined) {
930 var $dialogContent = $target.parents('.dialogContent');
931 $target.data('isOverlayDropdownButton', $dialogContent.length > 0);
932
933 if ($dialogContent.length) {
934 $dialogContent.on('scroll', this._dialogScroll);
935 }
936 }
937
2b8f0792
AE
938 // close all dropdowns
939 for (var $containerID in this._dropdowns) {
940 var $dropdown = this._dropdowns[$containerID];
3ef8dee9
AE
941 var $dropdownMenu = this._menus[$containerID];
942
2b8f0792
AE
943 if ($dropdown.hasClass('dropdownOpen')) {
944 $dropdown.removeClass('dropdownOpen');
3ef8dee9
AE
945 $dropdownMenu.removeClass('dropdownOpen');
946
947 this._notifyCallbacks($containerID, 'close');
2b8f0792
AE
948 }
949 else if ($containerID === $targetID) {
950 $dropdown.addClass('dropdownOpen');
3ef8dee9
AE
951 $dropdownMenu.addClass('dropdownOpen');
952
953 this._notifyCallbacks($containerID, 'open');
71662ae8 954
3ef8dee9 955 this.setAlignment($dropdown, $dropdownMenu);
2b8f0792 956 }
184a8d6d
AE
957 }
958
38d131ce
AE
959 if (event !== null) {
960 event.stopPropagation();
961 return false;
962 }
184a8d6d
AE
963 },
964
3ef8dee9
AE
965 /**
966 * Toggles a dropdown.
967 *
968 * @param string containerID
969 */
970 toggleDropdown: function(containerID) {
971 this._toggle(null, containerID);
972 },
973
974 /**
975 * Returns dropdown by container id.
976 *
977 * @param string containerID
978 * @return jQuery
979 */
980 getDropdown: function(containerID) {
981 if (this._dropdowns[containerID]) {
982 return this._dropdowns[containerID];
983 }
984
985 return null;
986 },
987
988 /**
989 * Returns dropdown menu by container id.
990 *
991 * @param string containerID
992 * @return jQuery
993 */
994 getDropdownMenu: function(containerID) {
995 if (this._menus[containerID]) {
996 return this._menus[containerID];
997 }
998
999 return null;
1000 },
1001
1002 /**
1003 * Sets alignment for given container id.
1004 *
1005 * @param string containerID
1006 */
1007 setAlignmentByID: function(containerID) {
1008 var $dropdown = this.getDropdown(containerID);
1009 if ($dropdown === null) {
1010 console.debug("[WCF.Dropdown] Unable to find dropdown identified by '" + containerID + "'");
1011 }
1012
1013 var $dropdownMenu = this.getDropdownMenu(containerID);
1014 if ($dropdownMenu === null) {
1015 console.debug("[WCF.Dropdown] Unable to find dropdown menu identified by '" + containerID + "'");
1016 }
1017
1018 this.setAlignment($dropdown, $dropdownMenu);
1019 },
1020
71662ae8
AE
1021 /**
1022 * Sets alignment for dropdown.
1023 *
1024 * @param jQuery dropdown
1025 * @param jQuery dropdownMenu
1026 */
1027 setAlignment: function(dropdown, dropdownMenu) {
253a1b74
AE
1028 // force dropdown menu to be placed in the upper left corner, otherwise
1029 // it might cause the calculations to be a bit off if the page exceeds
1030 // the window boundaries during getDimensions() making it visible
1031 if (!dropdownMenu.data('isInitialized')) {
1032 dropdownMenu.data('isInitialized', true).css({ left: 0, top: 0 });
1033 }
1034
3ef8dee9
AE
1035 // get dropdown position
1036 var $dropdownDimensions = dropdown.getDimensions('outer');
1037 var $dropdownOffsets = dropdown.getOffsets('offset');
1038 var $menuDimensions = dropdownMenu.getDimensions('outer');
71662ae8
AE
1039 var $windowWidth = $(window).width();
1040
ca155b1c
AE
1041 // check if button belongs to an i18n textarea
1042 var $button = dropdown.find('.dropdownToggle');
1043 if ($button.hasClass('dropdownCaptionTextarea')) {
1044 // use button dimensions instead
1045 $dropdownDimensions = $button.getDimensions('outer');
1046 }
1047
1635b5bd
MS
1048 // get alignment
1049 var $align = 'left';
1050 if (($dropdownOffsets.left + $menuDimensions.width) > $windowWidth) {
1051 $align = 'right';
71662ae8 1052 }
3ef8dee9 1053
1635b5bd
MS
1054 // calculate offsets
1055 var $left = 'auto';
1056 var $right = 'auto';
1057 if ($align === 'left') {
9db77c5d 1058 dropdownMenu.removeClass('dropdownArrowRight');
3ef8dee9 1059
1635b5bd
MS
1060 $left = $dropdownOffsets.left + 'px';
1061 }
1062 else {
1063 dropdownMenu.addClass('dropdownArrowRight');
3ef8dee9 1064
1635b5bd 1065 $right = ($windowWidth - ($dropdownOffsets.left + $dropdownDimensions.width)) + 'px';
71662ae8 1066 }
1635b5bd 1067
9db77c5d
AE
1068 // calculate vertical offset
1069 var $wasHidden = true;
1070 if (dropdownMenu.hasClass('dropdownOpen')) {
1071 $wasHidden = false;
1072 dropdownMenu.removeClass('dropdownOpen');
1073 }
1074
1075 var $bottom = 'auto';
1076 var $top = $dropdownOffsets.top + $dropdownDimensions.height + 7;
f3c5fca9 1077 if ($top + $menuDimensions.height > $(window).height() + $(document).scrollTop()) {
9db77c5d
AE
1078 $bottom = $(window).height() - $dropdownOffsets.top + 10;
1079 $top = 'auto';
1080
1081 dropdownMenu.addClass('dropdownArrowBottom');
1082 }
1083 else {
1084 dropdownMenu.removeClass('dropdownArrowBottom');
1085 }
1086
1087 if (!$wasHidden) {
1088 dropdownMenu.addClass('dropdownOpen');
1089 }
1090
1635b5bd 1091 dropdownMenu.css({
9db77c5d 1092 bottom: $bottom,
1635b5bd
MS
1093 left: $left,
1094 right: $right,
9db77c5d 1095 top: $top
1635b5bd 1096 });
71662ae8
AE
1097 },
1098
184a8d6d
AE
1099 /**
1100 * Closes all dropdowns.
1101 */
1102 _closeAll: function() {
1103 for (var $containerID in this._dropdowns) {
1104 var $dropdown = this._dropdowns[$containerID];
1105 if ($dropdown.hasClass('dropdownOpen')) {
1106 $dropdown.removeClass('dropdownOpen');
3ef8dee9 1107 this._menus[$containerID].removeClass('dropdownOpen');
184a8d6d 1108
3ef8dee9 1109 this._notifyCallbacks($containerID, 'close');
184a8d6d
AE
1110 }
1111 }
1112 },
1113
1114 /**
1115 * Closes a dropdown without notifying callbacks.
1116 *
1117 * @param string containerID
1118 */
1119 close: function(containerID) {
1120 if (!this._dropdowns[containerID]) {
1121 return;
1122 }
1123
3ef8dee9
AE
1124 this._dropdowns[containerID].removeClass('dropdownMenu');
1125 this._menus[containerID].removeClass('dropdownMenu');
184a8d6d
AE
1126 },
1127
1128 /**
1129 * Notifies callbacks.
1130 *
3ef8dee9 1131 * @param string containerID
184a8d6d
AE
1132 * @param string action
1133 */
3ef8dee9
AE
1134 _notifyCallbacks: function(containerID, action) {
1135 if (!this._callbacks[containerID]) {
184a8d6d
AE
1136 return;
1137 }
1138
3ef8dee9
AE
1139 for (var $i = 0, $length = this._callbacks[containerID].length; $i < $length; $i++) {
1140 this._callbacks[containerID][$i](containerID, action);
184a8d6d
AE
1141 }
1142 }
1143};
5be96f42 1144
184a8d6d 1145/**
0d6ea23f
AE
1146 * Clipboard API
1147 */
1148WCF.Clipboard = {
1149 /**
1150 * action proxy object
1151 * @var WCF.Action.Proxy
1152 */
1153 _actionProxy: null,
1154
da27d58a
MS
1155 /**
1156 * action objects
1157 * @var object
1158 */
1159 _actionObjects: {},
1160
d322363b
AE
1161 /**
1162 * list of clipboard containers
1163 * @var jQuery
1164 */
67bf4eb3 1165 _containers: null,
d322363b 1166
d893cd14
AE
1167 /**
1168 * container meta data
1169 * @var object
1170 */
1171 _containerData: { },
1172
d322363b
AE
1173 /**
1174 * user has marked items
1175 * @var boolean
1176 */
1177 _hasMarkedItems: false,
1178
a009a689 1179 /**
5966cc7f
MS
1180 * list of ids of marked objects grouped by object type
1181 * @var object
a009a689 1182 */
5966cc7f 1183 _markedObjectIDs: { },
a009a689 1184
0d6ea23f
AE
1185 /**
1186 * current page
1187 * @var string
1188 */
1189 _page: '',
1190
607c1f32
AE
1191 /**
1192 * current page's object id
1193 * @var integer
1194 */
1195 _pageObjectID: 0,
1196
0d6ea23f
AE
1197 /**
1198 * proxy object
1199 * @var WCF.Action.Proxy
1200 */
1201 _proxy: null,
1202
f892f868
AE
1203 /**
1204 * list of elements already tracked for clipboard actions
1205 * @var object
1206 */
1207 _trackedElements: { },
1208
0d6ea23f
AE
1209 /**
1210 * Initializes the clipboard API.
607c1f32
AE
1211 *
1212 * @param string page
1213 * @param integer hasMarkedItems
1214 * @param object actionObjects
1215 * @param integer pageObjectID
0d6ea23f 1216 */
607c1f32 1217 init: function(page, hasMarkedItems, actionObjects, pageObjectID) {
0d6ea23f 1218 this._page = page;
607c1f32
AE
1219 this._actionObjects = actionObjects || { };
1220 this._hasMarkedItems = (hasMarkedItems > 0);
1221 this._pageObjectID = parseInt(pageObjectID) || 0;
0d6ea23f
AE
1222
1223 this._actionProxy = new WCF.Action.Proxy({
1224 success: $.proxy(this._actionSuccess, this),
d71e5a29 1225 url: 'index.php/ClipboardProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND
0d6ea23f
AE
1226 });
1227
1228 this._proxy = new WCF.Action.Proxy({
1229 success: $.proxy(this._success, this),
d71e5a29 1230 url: 'index.php/Clipboard/?t=' + SECURITY_TOKEN + SID_ARG_2ND
0d6ea23f
AE
1231 });
1232
1233 // init containers first
6f475a52 1234 this._containers = $('.jsClipboardContainer').each($.proxy(function(index, container) {
0d6ea23f
AE
1235 this._initContainer(container);
1236 }, this));
d322363b
AE
1237
1238 // loads marked items
8b0cabda 1239 if (this._hasMarkedItems && this._containers.length) {
d322363b
AE
1240 this._loadMarkedItems();
1241 }
f892f868
AE
1242
1243 var self = this;
1244 WCF.DOMNodeInsertedHandler.addCallback('WCF.Clipboard', function() {
1245 self._containers = $('.jsClipboardContainer').each($.proxy(function(index, container) {
1246 self._initContainer(container);
1247 }, self));
1248 });
d322363b
AE
1249 },
1250
1251 /**
1252 * Loads marked items on init.
1253 */
1254 _loadMarkedItems: function() {
1255 new WCF.Action.Proxy({
1256 autoSend: true,
1257 data: {
d893cd14 1258 containerData: this._containerData,
607c1f32
AE
1259 pageClassName: this._page,
1260 pageObjectID: this._pageObjectID
d322363b
AE
1261 },
1262 success: $.proxy(this._loadMarkedItemsSuccess, this),
d71e5a29 1263 url: 'index.php/ClipboardLoadMarkedItems/?t=' + SECURITY_TOKEN + SID_ARG_2ND
d322363b
AE
1264 });
1265 },
1266
92a6d759
AE
1267 /**
1268 * Reloads the list of marked items.
1269 */
1270 reload: function() {
67bf4eb3
MS
1271 if (this._containers === null) {
1272 return;
1273 }
1274
92a6d759
AE
1275 this._loadMarkedItems();
1276 },
1277
d322363b
AE
1278 /**
1279 * Marks all returned items as marked
1280 *
1281 * @param object data
1282 * @param string textStatus
1283 * @param jQuery jqXHR
1284 */
1285 _loadMarkedItemsSuccess: function(data, textStatus, jqXHR) {
91b5b09f
AE
1286 this._resetMarkings();
1287
d322363b 1288 for (var $typeName in data.markedItems) {
7ce2effc 1289 if (!this._markedObjectIDs[$typeName]) {
7412629a 1290 this._markedObjectIDs[$typeName] = [ ];
7ce2effc
MS
1291 }
1292
d322363b 1293 var $objectData = data.markedItems[$typeName];
d322363b 1294 for (var $i in $objectData) {
5966cc7f 1295 this._markedObjectIDs[$typeName].push($objectData[$i]);
d322363b
AE
1296 }
1297
1298 // loop through all containers
a009a689 1299 this._containers.each($.proxy(function(index, container) {
d322363b
AE
1300 var $container = $(container);
1301
1302 // typeName does not match, continue
1303 if ($container.data('type') != $typeName) {
1304 return true;
1305 }
1306
1307 // mark items as marked
a009a689 1308 $container.find('input.jsClipboardItem').each($.proxy(function(innerIndex, item) {
d322363b 1309 var $item = $(item);
5966cc7f 1310 if (WCF.inArray($item.data('objectID'), this._markedObjectIDs[$typeName])) {
d1265cbf 1311 $item.prop('checked', true);
483aaa32
AE
1312
1313 // add marked class for element container
1314 $item.parents('.jsClipboardObject').addClass('jsMarked');
d322363b 1315 }
a009a689 1316 }, this));
d322363b
AE
1317
1318 // check if there is a markAll-checkbox
6f475a52 1319 $container.find('input.jsClipboardMarkAll').each(function(innerIndex, markAll) {
d322363b
AE
1320 var $allItemsMarked = true;
1321
6f475a52 1322 $container.find('input.jsClipboardItem').each(function(itemIndex, item) {
d322363b 1323 var $item = $(item);
1a50aae0 1324 if (!$item.prop('checked')) {
d322363b
AE
1325 $allItemsMarked = false;
1326 }
1327 });
1328
1329 if ($allItemsMarked) {
d1265cbf 1330 $(markAll).prop('checked', true);
d322363b
AE
1331 }
1332 });
a009a689 1333 }, this));
d322363b
AE
1334 }
1335
1336 // call success method to build item list editors
1337 this._success(data, textStatus, jqXHR);
0d6ea23f
AE
1338 },
1339
91b5b09f
AE
1340 /**
1341 * Resets all checkboxes.
1342 */
1343 _resetMarkings: function() {
5966cc7f 1344 this._containers.each($.proxy(function(index, container) {
91b5b09f
AE
1345 var $container = $(container);
1346
5966cc7f 1347 this._markedObjectIDs[$container.data('type')] = [ ];
d1265cbf 1348 $container.find('input.jsClipboardItem, input.jsClipboardMarkAll').prop('checked', false);
483aaa32 1349 $container.find('.jsClipboardObject').removeClass('jsMarked');
5966cc7f 1350 }, this));
91b5b09f
AE
1351 },
1352
0d6ea23f
AE
1353 /**
1354 * Initializes a clipboard container.
1355 *
1356 * @param object container
1357 */
1358 _initContainer: function(container) {
1359 var $container = $(container);
d893cd14 1360 var $containerID = $container.wcfIdentify();
0d6ea23f 1361
f892f868
AE
1362 if (!this._trackedElements[$containerID]) {
1363 $container.find('.jsClipboardMarkAll').data('hasContainer', $containerID).click($.proxy(this._markAll, this));
1364
5966cc7f 1365 this._markedObjectIDs[$container.data('type')] = [ ];
f892f868
AE
1366 this._containerData[$container.data('type')] = {};
1367 $.each($container.data(), $.proxy(function(index, element) {
1368 if (index.match(/^type(.+)/)) {
1369 this._containerData[$container.data('type')][WCF.String.lcfirst(index.replace(/^type/, ''))] = element;
1370 }
1371 }, this));
1372
1373 this._trackedElements[$containerID] = [ ];
1374 }
0d6ea23f 1375
f892f868
AE
1376 // track individual checkboxes
1377 $container.find('input.jsClipboardItem').each($.proxy(function(index, input) {
1378 var $input = $(input);
1379 var $inputID = $input.wcfIdentify();
1380
1381 if (!WCF.inArray($inputID, this._trackedElements[$containerID])) {
1382 this._trackedElements[$containerID].push($inputID);
1383
1384 $input.data('hasContainer', $containerID).click($.proxy(this._click, this));
995afaf1
MS
1385 }
1386 }, this));
0d6ea23f
AE
1387 },
1388
1389 /**
1390 * Processes change checkbox state.
1391 *
1392 * @param object event
1393 */
1394 _click: function(event) {
1395 var $item = $(event.target);
1396 var $objectID = $item.data('objectID');
1a50aae0 1397 var $isMarked = ($item.prop('checked')) ? true : false;
0d6ea23f
AE
1398 var $objectIDs = [ $objectID ];
1399
5966cc7f
MS
1400 if ($item.data('hasContainer')) {
1401 var $container = $('#' + $item.data('hasContainer'));
1402 var $type = $container.data('type');
1403 }
1404 else {
1405 var $type = $item.data('type');
1406 }
1407
a009a689 1408 if ($isMarked) {
5966cc7f 1409 this._markedObjectIDs[$type].push($objectID);
483aaa32 1410 $item.parents('.jsClipboardObject').addClass('jsMarked');
a009a689
MS
1411 }
1412 else {
5966cc7f 1413 this._markedObjectIDs[$type] = $.removeArrayValue(this._markedObjectIDs[$type], $objectID);
483aaa32 1414 $item.parents('.jsClipboardObject').removeClass('jsMarked');
a009a689
MS
1415 }
1416
0d6ea23f
AE
1417 // item is part of a container
1418 if ($item.data('hasContainer')) {
0d6ea23f
AE
1419 // check if all items are marked
1420 var $markedAll = true;
6f475a52 1421 $container.find('input.jsClipboardItem').each(function(index, containerItem) {
0d6ea23f 1422 var $containerItem = $(containerItem);
1a50aae0 1423 if (!$containerItem.prop('checked')) {
0d6ea23f
AE
1424 $markedAll = false;
1425 }
1426 });
1427
1428 // simulate a ticked 'markAll' checkbox
6f475a52 1429 $container.find('.jsClipboardMarkAll').each(function(index, markAll) {
0d6ea23f 1430 if ($markedAll) {
d1265cbf 1431 $(markAll).prop('checked', true);
0d6ea23f
AE
1432 }
1433 else {
d1265cbf 1434 $(markAll).prop('checked', false);
0d6ea23f
AE
1435 }
1436 });
1437 }
0d6ea23f
AE
1438
1439 this._saveState($type, $objectIDs, $isMarked);
1440 },
1441
1442 /**
1443 * Marks all associated clipboard items as checked.
1444 *
1445 * @param object event
1446 */
1447 _markAll: function(event) {
1448 var $item = $(event.target);
1449 var $objectIDs = [ ];
1450 var $isMarked = true;
1451
1452 // if markAll object is a checkbox, allow toggling
404c9abe 1453 if ($item.is('input')) {
1a50aae0 1454 $isMarked = $item.prop('checked');
0d6ea23f
AE
1455 }
1456
0d6ea23f
AE
1457 if ($item.data('hasContainer')) {
1458 var $container = $('#' + $item.data('hasContainer'));
1459 var $type = $container.data('type');
5966cc7f
MS
1460 }
1461 else {
1462 var $type = $item.data('type');
1463 }
1464
1465 // handle item containers
1466 if ($item.data('hasContainer')) {
0d6ea23f 1467 // toggle state for all associated items
a009a689 1468 $container.find('input.jsClipboardItem').each($.proxy(function(index, containerItem) {
0d6ea23f 1469 var $containerItem = $(containerItem);
a009a689 1470 var $objectID = $containerItem.data('objectID');
0d6ea23f 1471 if ($isMarked) {
1a50aae0 1472 if (!$containerItem.prop('checked')) {
d1265cbf 1473 $containerItem.prop('checked', true);
5966cc7f 1474 this._markedObjectIDs[$type].push($objectID);
a009a689 1475 $objectIDs.push($objectID);
0d6ea23f
AE
1476 }
1477 }
1478 else {
1a50aae0 1479 if ($containerItem.prop('checked')) {
d1265cbf 1480 $containerItem.prop('checked', false);
5966cc7f 1481 this._markedObjectIDs[$type] = $.removeArrayValue(this._markedObjectIDs[$type], $objectID);
a009a689 1482 $objectIDs.push($objectID);
0d6ea23f
AE
1483 }
1484 }
a009a689 1485 }, this));
483aaa32
AE
1486
1487 if ($isMarked) {
1488 $container.find('.jsClipboardObject').addClass('jsMarked');
1489 }
1490 else {
1491 $container.find('.jsClipboardObject').removeClass('jsMarked');
1492 }
0d6ea23f
AE
1493 }
1494
1495 // save new status
1496 this._saveState($type, $objectIDs, $isMarked);
1497 },
1498
1499 /**
1500 * Saves clipboard item state.
1501 *
1502 * @param string type
1503 * @param array objectIDs
1504 * @param boolean isMarked
1505 */
1506 _saveState: function(type, objectIDs, isMarked) {
1507 this._proxy.setOption('data', {
1508 action: (isMarked) ? 'mark' : 'unmark',
75a93a59 1509 containerData: this._containerData,
0d6ea23f
AE
1510 objectIDs: objectIDs,
1511 pageClassName: this._page,
1f79c900 1512 pageObjectID: this._pageObjectID,
0d6ea23f 1513 type: type
f4126129 1514 });
0d6ea23f
AE
1515 this._proxy.sendRequest();
1516 },
1517
1518 /**
1519 * Updates editor options.
1520 *
1521 * @param object data
1522 * @param string textStatus
1523 * @param jQuery jqXHR
1524 */
1525 _success: function(data, textStatus, jqXHR) {
1526 // clear all editors first
1527 var $containers = {};
6f475a52 1528 $('.jsClipboardEditor').each(function(index, container) {
0d6ea23f 1529 var $container = $(container);
1b4e9186
AE
1530 var $types = eval($container.data('types'));
1531 for (var $i = 0, $length = $types.length; $i < $length; $i++) {
1532 var $typeName = $types[$i];
0d6ea23f
AE
1533 $containers[$typeName] = $container;
1534 }
1535
b3991cb3
AE
1536 var $containerID = $container.wcfIdentify();
1537 WCF.CloseOverlayHandler.removeCallback($containerID);
ab7de4fd 1538
0d6ea23f
AE
1539 $container.empty();
1540 });
1541
8f3284a3 1542 // do not build new editors
0d6ea23f
AE
1543 if (!data.items) return;
1544
1545 // rebuild editors
1546 for (var $typeName in data.items) {
1547 if (!$containers[$typeName]) {
1548 continue;
1549 }
1550
1551 // create container
1552 var $container = $containers[$typeName];
1b4e9186
AE
1553 var $list = $container.children('ul');
1554 if ($list.length == 0) {
3ef8dee9 1555 $list = $('<ul />').appendTo($container);
1b4e9186
AE
1556 }
1557
0d6ea23f 1558 var $editor = data.items[$typeName];
3ef8dee9 1559 var $label = $('<li class="dropdown"><span class="dropdownToggle button">' + $editor.label + '</span></li>').appendTo($list);
844e6560
AE
1560 var $itemList = $('<ol class="dropdownMenu"></ol>').appendTo($label);
1561
0d6ea23f
AE
1562 // create editor items
1563 for (var $itemIndex in $editor.items) {
1564 var $item = $editor.items[$itemIndex];
49c164a8 1565
844e6560 1566 var $listItem = $('<li><span>' + $item.label + '</span></li>').appendTo($itemList);
6eae9331 1567 $listItem.data('container', $container);
da27d58a 1568 $listItem.data('objectType', $typeName);
0d6ea23f
AE
1569 $listItem.data('actionName', $item.actionName).data('parameters', $item.parameters);
1570 $listItem.data('internalData', $item.internalData).data('url', $item.url).data('type', $typeName);
1571
1572 // bind event
1573 $listItem.click($.proxy(this._executeAction, this));
1574 }
9f959ced 1575
77f8b178
AE
1576 // add 'unmark all'
1577 $('<li class="dropdownDivider" />').appendTo($itemList);
1578 $('<li><span>' + WCF.Language.get('wcf.clipboard.item.unmarkAll') + '</span></li>').appendTo($itemList).click($.proxy(function() {
1579 this._proxy.setOption('data', {
1580 action: 'unmarkAll',
1581 type: $typeName
1582 });
1583 this._proxy.setOption('success', $.proxy(function(data, textStatus, jqXHR) {
1584 for (var $__containerID in this._containers) {
1585 var $__container = $(this._containers[$__containerID]);
1586 if ($__container.data('type') == $typeName) {
d1265cbf 1587 $__container.find('.jsClipboardMarkAll, .jsClipboardItem').prop('checked', false);
933a33bb
MS
1588 $__container.find('.jsClipboardObject').removeClass('jsMarked');
1589
77f8b178
AE
1590 break;
1591 }
1592 }
1593
1594 // call and restore success method
1595 this._success(data, textStatus, jqXHR);
1596 this._proxy.setOption('success', $.proxy(this._success, this));
1597 }, this));
1598 this._proxy.sendRequest();
1599 }, this));
1600
3ef8dee9 1601 WCF.Dropdown.initDropdown($label.children('.dropdownToggle'), false);
0d6ea23f
AE
1602 }
1603 },
da27d58a
MS
1604
1605 /**
1606 * Closes the clipboard editor item list.
1607 */
b3991cb3 1608 _closeLists: function() {
d27e2667 1609 $('.jsClipboardEditor ul').removeClass('dropdownOpen');
b3991cb3 1610 },
0d6ea23f
AE
1611
1612 /**
1613 * Executes a clipboard editor item action.
1614 *
1615 * @param object event
1616 */
1617 _executeAction: function(event) {
9ce295a0 1618 var $listItem = $(event.currentTarget);
0d6ea23f
AE
1619 var $url = $listItem.data('url');
1620 if ($url) {
1621 window.location.href = $url;
1622 }
1623
49c164a8
AE
1624 if ($listItem.data('parameters').className && $listItem.data('parameters').actionName) {
1625 if ($listItem.data('parameters').actionName === 'unmarkAll' || $listItem.data('parameters').objectIDs) {
1626 var $confirmMessage = $listItem.data('internalData')['confirmMessage'];
1627 if ($confirmMessage) {
1628 var $template = $listItem.data('internalData')['template'];
1629 if ($template) $template = $($template);
1630
1631 WCF.System.Confirmation.show($confirmMessage, $.proxy(function(action) {
1632 if (action === 'confirm') {
1633 var $data = { };
1634
1635 if ($template && $template.length) {
1636 $('#wcfSystemConfirmationContent').find('input, select, textarea').each(function(index, item) {
1637 var $item = $(item);
1638 $data[$item.prop('name')] = $item.val();
1639 });
1640 }
1641
1642 this._executeAJAXActions($listItem, $data);
db6b20b8 1643 }
49c164a8
AE
1644 }, this), '', $template);
1645 }
1646 else {
1647 this._executeAJAXActions($listItem, { });
1648 }
da27d58a 1649 }
a009a689
MS
1650 }
1651
0d6ea23f 1652 // fire event
6eae9331 1653 $listItem.data('container').trigger('clipboardAction', [ $listItem.data('type'), $listItem.data('actionName'), $listItem.data('parameters') ]);
0d6ea23f
AE
1654 },
1655
da27d58a
MS
1656 /**
1657 * Executes the AJAX actions for the given editor list item.
1658 *
1659 * @param jQuery listItem
db6b20b8 1660 * @param object data
da27d58a 1661 */
db6b20b8
AE
1662 _executeAJAXActions: function(listItem, data) {
1663 data = data || { };
1664 var $objectIDs = [];
49c164a8
AE
1665 if (listItem.data('parameters').actionName !== 'unmarkAll') {
1666 $.each(listItem.data('parameters').objectIDs, function(index, objectID) {
1667 $objectIDs.push(parseInt(objectID));
1668 });
1669 }
db6b20b8 1670
54c91463 1671 var $parameters = {
438b4f4d
MS
1672 data: data,
1673 containerData: this._containerData[listItem.data('type')]
54c91463 1674 };
faa1fd02 1675 var $__parameters = listItem.data('internalData')['parameters'];
54c91463
AE
1676 if ($__parameters !== undefined) {
1677 for (var $key in $__parameters) {
1678 $parameters[$key] = $__parameters[$key];
1679 }
1680 }
1681
da27d58a
MS
1682 new WCF.Action.Proxy({
1683 autoSend: true,
1684 data: {
1685 actionName: listItem.data('parameters').actionName,
1686 className: listItem.data('parameters').className,
db6b20b8 1687 objectIDs: $objectIDs,
54c91463 1688 parameters: $parameters
da27d58a 1689 },
1ecfbb69 1690 success: $.proxy(function(data) {
49c164a8 1691 if (listItem.data('parameters').actionName !== 'unmarkAll') {
6eae9331 1692 listItem.data('container').trigger('clipboardActionResponse', [ data, listItem.data('type'), listItem.data('actionName'), listItem.data('parameters') ]);
49c164a8 1693 }
1ecfbb69
AE
1694
1695 this._loadMarkedItems();
91b5b09f 1696 }, this)
da27d58a
MS
1697 });
1698
1699 if (this._actionObjects[listItem.data('objectType')] && this._actionObjects[listItem.data('objectType')][listItem.data('parameters').actionName]) {
05710b5d 1700 this._actionObjects[listItem.data('objectType')][listItem.data('parameters').actionName].triggerEffect($objectIDs);
da27d58a
MS
1701 }
1702 },
1703
0d6ea23f
AE
1704 /**
1705 * Sends a clipboard proxy request.
1706 *
1707 * @param object item
1708 */
1709 sendRequest: function(item) {
1710 var $item = $(item);
1711
1712 this._actionProxy.setOption('data', {
1713 parameters: $item.data('parameters'),
1714 typeName: $item.data('type')
1715 });
1716 this._actionProxy.sendRequest();
1717 }
1718};
1719
158bd3ca
TD
1720/**
1721 * Provides a simple call for periodical executed functions. Based upon
1722 * ideas by Prototype's PeriodicalExecuter.
1723 *
1724 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/periodical_executer.js
1725 * @param function callback
1726 * @param integer delay
1727 */
39e27190 1728WCF.PeriodicalExecuter = Class.extend({
ac37b8fe
AE
1729 /**
1730 * callback for each execution cycle
1731 * @var object
1732 */
1733 _callback: null,
1734
1735 /**
1736 * interval id
9f959ced 1737 * @var integer
ac37b8fe
AE
1738 */
1739 _intervalID: null,
1740
1741 /**
1742 * execution state
1743 * @var boolean
1744 */
1745 _isExecuting: false,
1746
158bd3ca
TD
1747 /**
1748 * Initializes a periodical executer.
1749 *
1750 * @param function callback
1751 * @param integer delay
1752 */
1753 init: function(callback, delay) {
ac37b8fe
AE
1754 if (!$.isFunction(callback)) {
1755 console.debug('[WCF.PeriodicalExecuter] Given callback is invalid, aborting.');
1756 return;
1757 }
158bd3ca 1758
ac37b8fe
AE
1759 this._callback = callback;
1760 this._intervalID = setInterval($.proxy(this._execute, this), delay);
158bd3ca
TD
1761 },
1762
1763 /**
1764 * Executes callback.
1765 */
1766 _execute: function() {
ac37b8fe
AE
1767 if (!this._isExecuting) {
1768 try {
1769 this._isExecuting = true;
1770 this._callback(this);
1771 this._isExecuting = false;
1772 }
1773 catch (e) {
1774 this._isExecuting = false;
1775 throw e;
1776 }
158bd3ca
TD
1777 }
1778 },
1779
1780 /**
1781 * Terminates loop.
1782 */
1783 stop: function() {
ac37b8fe
AE
1784 if (!this._intervalID) {
1785 return;
1786 }
1787
1788 clearInterval(this._intervalID);
158bd3ca 1789 }
39e27190 1790});
158bd3ca
TD
1791
1792/**
093162cc 1793 * Handler for loading overlays
158bd3ca 1794 */
093162cc 1795WCF.LoadingOverlayHandler = {
b3991cb3 1796 /**
093162cc 1797 * count of active loading-requests
b3991cb3
AE
1798 * @var integer
1799 */
1800 _activeRequests: 0,
9f959ced 1801
b3991cb3
AE
1802 /**
1803 * loading overlay
1804 * @var jQuery
1805 */
1806 _loadingOverlay: null,
9f959ced 1807
526f87f2
AE
1808 /**
1809 * WCF.PeriodicalExecuter instance
1810 * @var WCF.PeriodicalExecuter
1811 */
1812 _pending: null,
1813
b3991cb3 1814 /**
093162cc 1815 * Adds one loading-request and shows the loading overlay if nessercery
b3991cb3 1816 */
093162cc
MK
1817 show: function() {
1818 if (this._loadingOverlay === null) { // create loading overlay on first run
1be839fc
AE
1819 this._loadingOverlay = $('<div class="spinner"><span class="icon icon48 icon-spinner" /> <span>' + WCF.Language.get('wcf.global.loading') + '</span></div>').appendTo($('body'));
1820
1821 // fix position
1822 var $width = this._loadingOverlay.outerWidth();
1823 if ($width < 70) $width = 70;
1824 this._loadingOverlay.css({
1825 marginLeft: Math.ceil(-1 * $width / 2),
1826 width: $width
1827 }).hide();
093162cc
MK
1828 }
1829
1830 this._activeRequests++;
1831 if (this._activeRequests == 1) {
526f87f2
AE
1832 if (this._pending === null) {
1833 var self = this;
1834 this._pending = new WCF.PeriodicalExecuter(function(pe) {
1835 if (self._activeRequests) {
1836 self._loadingOverlay.stop(true, true).fadeIn(100);
1837 }
1838
1839 pe.stop();
1840 self._pending = null;
1841 }, 250);
1842 }
1843
093162cc
MK
1844 }
1845 },
9f959ced 1846
b3991cb3 1847 /**
093162cc
MK
1848 * Removes one loading-request and hides loading overlay if there're no more pending requests
1849 */
1850 hide: function() {
1851 this._activeRequests--;
1852 if (this._activeRequests == 0) {
526f87f2
AE
1853 if (this._pending !== null) {
1854 this._pending.stop();
1855 this._pending = null;
1856 }
1857
093162cc
MK
1858 this._loadingOverlay.stop(true, true).fadeOut(100);
1859 }
4dbe4dc2
MK
1860 },
1861
1862 /**
1863 * Updates a icon to/from spinner
1864 *
1865 * @param jQuery target
1866 * @pram boolean loading
1867 */
1868 updateIcon: function(target, loading) {
1869 var $method = (loading === undefined || loading ? 'addClass' : 'removeClass');
1870
1871 target.find('.icon')[$method]('icon-spinner');
1872 if (target.hasClass('icon')) {
1873 target[$method]('icon-spinner');
1874 }
093162cc
MK
1875 }
1876};
1877
1878/**
1879 * Namespace for AJAXProxies
1880 */
1881WCF.Action = {};
1882
1883/**
1884 * Basic implementation for AJAX-based proxyies
1885 *
1886 * @param object options
1887 */
1888WCF.Action.Proxy = Class.extend({
906c8f23
AE
1889 /**
1890 * shows loading overlay for a single request
1891 * @var boolean
1892 */
1893 _showLoadingOverlayOnce: false,
1894
2f326e85
AE
1895 /**
1896 * suppresses errors
1897 * @var boolean
1898 */
1899 _suppressErrors: false,
9f959ced 1900
9ee50dd7
MK
1901 /**
1902 * last request
1903 * @var jqXHR
1904 */
1905 _lastRequest: null,
1906
158bd3ca
TD
1907 /**
1908 * Initializes AJAXProxy.
1909 *
1910 * @param object options
1911 */
1912 init: function(options) {
1913 // initialize default values
1914 this.options = $.extend(true, {
1915 autoSend: false,
1916 data: { },
944d7f97 1917 dataType: 'json',
158bd3ca
TD
1918 after: null,
1919 init: null,
889cdd4c 1920 jsonp: 'callback',
7965fc49 1921 async: true,
158bd3ca 1922 failure: null,
324e8301 1923 showLoadingOverlay: true,
158bd3ca 1924 success: null,
88432fe0 1925 suppressErrors: false,
158bd3ca 1926 type: 'POST',
9ee50dd7
MK
1927 url: 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND,
1928 aborted: null,
1929 autoAbortPrevious: false
158bd3ca
TD
1930 }, options);
1931
1932 this.confirmationDialog = null;
1933 this.loading = null;
906c8f23 1934 this._showLoadingOverlayOnce = false;
9b1326f4 1935 this._suppressErrors = (this.options.suppressErrors === true);
158bd3ca
TD
1936
1937 // send request immediately after initialization
1938 if (this.options.autoSend) {
1939 this.sendRequest();
1940 }
2f326e85
AE
1941
1942 var self = this;
1943 $(window).on('beforeunload', function() { self._suppressErrors = true; });
158bd3ca
TD
1944 },
1945
1946 /**
1947 * Sends an AJAX request.
9ee50dd7
MK
1948 *
1949 * @param abortPrevious boolean
1950 * @return jqXHR
158bd3ca 1951 */
9ee50dd7 1952 sendRequest: function(abortPrevious) {
158bd3ca
TD
1953 this._init();
1954
9ee50dd7
MK
1955 if (abortPrevious || this.options.autoAbortPrevious) {
1956 this.abortPrevious();
1957 }
1958
1959 this._lastRequest = $.ajax({
158bd3ca 1960 data: this.options.data,
944d7f97 1961 dataType: this.options.dataType,
889cdd4c 1962 jsonp: this.options.jsonp,
7965fc49 1963 async: this.options.async,
158bd3ca
TD
1964 type: this.options.type,
1965 url: this.options.url,
1966 success: $.proxy(this._success, this),
1967 error: $.proxy(this._failure, this)
1968 });
9ee50dd7
MK
1969 return this._lastRequest;
1970 },
1971
1972 /**
1973 * Aborts the previous request
1974 */
1975 abortPrevious: function() {
1976 if (this._lastRequest !== null) {
1977 this._lastRequest.abort();
39487b35 1978 this._lastRequest = null;
9ee50dd7 1979 }
158bd3ca
TD
1980 },
1981
906c8f23
AE
1982 /**
1983 * Shows loading overlay for a single request.
1984 */
1985 showLoadingOverlayOnce: function() {
1986 this._showLoadingOverlayOnce = true;
1987 },
1988
88432fe0
AE
1989 /**
1990 * Suppressed errors for this action proxy.
1991 */
1992 suppressErrors: function() {
1993 this._suppressErrors = true;
1994 },
1995
158bd3ca
TD
1996 /**
1997 * Fires before request is send, displays global loading status.
1998 */
1999 _init: function() {
2000 if ($.isFunction(this.options.init)) {
06c820f0 2001 this.options.init(this);
158bd3ca
TD
2002 }
2003
906c8f23 2004 if (this.options.showLoadingOverlay || this._showLoadingOverlayOnce) {
093162cc 2005 WCF.LoadingOverlayHandler.show();
b3991cb3 2006 }
158bd3ca
TD
2007 },
2008
2009 /**
2010 * Handles AJAX errors.
2011 *
2012 * @param object jqXHR
2013 * @param string textStatus
2014 * @param string errorThrown
2015 */
2016 _failure: function(jqXHR, textStatus, errorThrown) {
9ee50dd7
MK
2017 if (textStatus == 'abort') {
2018 // call child method if applicable
2019 if ($.isFunction(this.options.aborted)) {
2020 this.options.aborted(jqXHR);
2021 }
2022
2023 return;
2024 }
2025
158bd3ca 2026 try {
c0aaa588 2027 var $data = $.parseJSON(jqXHR.responseText);
158bd3ca
TD
2028
2029 // call child method if applicable
2f326e85 2030 var $showError = true;
158bd3ca 2031 if ($.isFunction(this.options.failure)) {
c0aaa588 2032 $showError = this.options.failure($data, jqXHR, textStatus, errorThrown);
158bd3ca
TD
2033 }
2034
2f326e85 2035 if (!this._suppressErrors && $showError !== false) {
40f9fe18
AE
2036 var $details = '';
2037 if ($data.stacktrace) $details = '<br /><p>Stacktrace:</p><p>' + $data.stacktrace + '</p>';
2038 else if ($data.exceptionID) $details = '<br /><p>Exception ID: <code>' + $data.exceptionID + '</code></p>';
2039
2040 $('<div class="ajaxDebugMessage"><p>' + $data.message + '</p>' + $details + '</div>').wcfDialog({ title: WCF.Language.get('wcf.global.error.title') });
2f326e85 2041 }
158bd3ca
TD
2042 }
2043 // failed to parse JSON
2044 catch (e) {
2f326e85
AE
2045 // call child method if applicable
2046 var $showError = true;
2047 if ($.isFunction(this.options.failure)) {
c0aaa588 2048 $showError = this.options.failure(null, jqXHR, textStatus, errorThrown);
2f326e85
AE
2049 }
2050
2051 if (!this._suppressErrors && $showError !== false) {
6ff02d16 2052 var $message = (textStatus === 'timeout') ? WCF.Language.get('wcf.global.error.timeout') : jqXHR.responseText;
c648d138
AE
2053
2054 // validate if $message is neither empty nor 'undefined'
2055 if ($message && $message != 'undefined') {
2056 $('<div class="ajaxDebugMessage"><p>' + $message + '</p></div>').wcfDialog({ title: WCF.Language.get('wcf.global.error.title') });
2057 }
2f326e85 2058 }
158bd3ca
TD
2059 }
2060
2061 this._after();
2062 },
2063
2064 /**
2065 * Handles successful AJAX requests.
2066 *
2067 * @param object data
2068 * @param string textStatus
2069 * @param object jqXHR
2070 */
2071 _success: function(data, textStatus, jqXHR) {
2072 // call child method if applicable
2073 if ($.isFunction(this.options.success)) {
9ae57972 2074 // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring
82b3f032 2075 if (data && data.returnValues && data.returnValues.template !== undefined) {
9ae57972
AE
2076 data.returnValues.template = $.trim(data.returnValues.template);
2077 }
2078
158bd3ca
TD
2079 this.options.success(data, textStatus, jqXHR);
2080 }
2081
2082 this._after();
2083 },
2084
2085 /**
2086 * Fires after an AJAX request, hides global loading status.
2087 */
2088 _after: function() {
9ee50dd7 2089 this._lastRequest = null;
158bd3ca
TD
2090 if ($.isFunction(this.options.after)) {
2091 this.options.after();
2092 }
350fad79 2093
906c8f23 2094 if (this.options.showLoadingOverlay || this._showLoadingOverlayOnce) {
093162cc 2095 WCF.LoadingOverlayHandler.hide();
906c8f23
AE
2096
2097 if (this._showLoadingOverlayOnce) {
2098 this._showLoadingOverlayOnce = false;
2099 }
093162cc
MK
2100 }
2101
42d7d2cc 2102 WCF.DOMNodeInsertedHandler.execute();
b95fdaea
AE
2103
2104 // fix anchor tags generated through WCF::getAnchor()
2105 $('a[href*=#]').each(function(index, link) {
2106 var $link = $(link);
8111944e 2107 if ($link.prop('href').indexOf('AJAXProxy') != -1) {
b95fdaea
AE
2108 var $anchor = $link.prop('href').substr($link.prop('href').indexOf('#'));
2109 var $pageLink = document.location.toString().replace(/#.*/, '');
2110 $link.prop('href', $pageLink + $anchor);
2111 }
2112 });
158bd3ca
TD
2113 },
2114
2115 /**
2116 * Sets options, MUST be used to set parameters before sending request
2117 * if calling from child classes.
2118 *
2119 * @param string optionName
2120 * @param mixed optionData
2121 */
2122 setOption: function(optionName, optionData) {
2123 this.options[optionName] = optionData;
2124 }
39e27190 2125});
158bd3ca
TD
2126
2127/**
2128 * Basic implementation for simple proxy access using bound elements.
2129 *
2130 * @param object options
2131 * @param object callbacks
2132 */
39e27190 2133WCF.Action.SimpleProxy = Class.extend({
158bd3ca
TD
2134 /**
2135 * Initializes SimpleProxy.
2136 *
2137 * @param object options
2138 * @param object callbacks
2139 */
2140 init: function(options, callbacks) {
2141 /**
2142 * action-specific options
2143 */
2144 this.options = $.extend(true, {
2145 action: '',
2146 className: '',
2147 elements: null,
2148 eventName: 'click'
2149 }, options);
2150
2151 /**
2152 * proxy-specific options
2153 */
2154 this.callbacks = $.extend(true, {
2155 after: null,
2156 failure: null,
2157 init: null,
2158 success: null
2159 }, callbacks);
2160
2161 if (!this.options.elements) return;
2162
2163 // initialize proxy
2164 this.proxy = new WCF.Action.Proxy(this.callbacks);
2165
2166 // bind event listener
2167 this.options.elements.each($.proxy(function(index, element) {
2168 $(element).bind(this.options.eventName, $.proxy(this._handleEvent, this));
2169 }, this));
2170 },
2171
2172 /**
2173 * Handles event actions.
2174 *
2175 * @param object event
2176 */
2177 _handleEvent: function(event) {
2178 this.proxy.setOption('data', {
2179 actionName: this.options.action,
2180 className: this.options.className,
2181 objectIDs: [ $(event.target).data('objectID') ]
2182 });
2183
2184 this.proxy.sendRequest();
2185 }
39e27190 2186});
158bd3ca
TD
2187
2188/**
2189 * Basic implementation for AJAXProxy-based deletion.
2190 *
2191 * @param string className
d371330f 2192 * @param string containerSelector
9d4465dd 2193 * @param string buttonSelector
158bd3ca 2194 */
5ffc72b6 2195WCF.Action.Delete = Class.extend({
9d4465dd
AE
2196 /**
2197 * delete button selector
2198 * @var string
2199 */
2200 _buttonSelector: '',
2201
d371330f
AE
2202 /**
2203 * action class name
2204 * @var string
2205 */
2206 _className: '',
2207
2208 /**
2209 * container selector
2210 * @var string
2211 */
2212 _containerSelector: '',
2213
2214 /**
2215 * list of known container ids
2216 * @var array<string>
2217 */
2218 _containers: [ ],
2219
158bd3ca
TD
2220 /**
2221 * Initializes 'delete'-Proxy.
2222 *
2223 * @param string className
d371330f 2224 * @param string containerSelector
9d4465dd 2225 * @param string buttonSelector
158bd3ca 2226 */
9d4465dd 2227 init: function(className, containerSelector, buttonSelector) {
d371330f
AE
2228 this._containerSelector = containerSelector;
2229 this._className = className;
9d4465dd
AE
2230 this._buttonSelector = (buttonSelector) ? buttonSelector : '.jsDeleteButton';
2231
d371330f 2232 this.proxy = new WCF.Action.Proxy({
158bd3ca 2233 success: $.proxy(this._success, this)
d371330f 2234 });
158bd3ca 2235
d371330f
AE
2236 this._initElements();
2237
2238 WCF.DOMNodeInsertedHandler.addCallback('WCF.Action.Delete' + this._className.hashCode(), $.proxy(this._initElements, this));
2239 },
2240
2241 /**
2242 * Initializes available element containers.
2243 */
2244 _initElements: function() {
2245 var self = this;
2246 $(this._containerSelector).each(function(index, container) {
2247 var $container = $(container);
2248 var $containerID = $container.wcfIdentify();
2249
2250 if (!WCF.inArray($containerID, self._containers)) {
2251 self._containers.push($containerID);
4ae603a5 2252 $container.find(self._buttonSelector).click($.proxy(self._click, self));
d371330f
AE
2253 }
2254 });
158bd3ca
TD
2255 },
2256
2257 /**
2258 * Sends AJAX request.
2259 *
2260 * @param object event
2261 */
2262 _click: function(event) {
eea52a29 2263 var $target = $(event.currentTarget);
b437bee4 2264 event.preventDefault();
158bd3ca
TD
2265
2266 if ($target.data('confirmMessage')) {
f02b3f75 2267 WCF.System.Confirmation.show($target.data('confirmMessage'), $.proxy(this._execute, this), { target: $target });
158bd3ca
TD
2268 }
2269 else {
4dbe4dc2 2270 WCF.LoadingOverlayHandler.updateIcon($target);
158bd3ca
TD
2271 this._sendRequest($target);
2272 }
f02b3f75
AE
2273 },
2274
90a29960
MS
2275 /**
2276 * Is called if the delete effect has been triggered on the given element.
2277 *
2278 * @param jQuery element
2279 */
2280 _didTriggerEffect: function(element) {
2281 // does nothing
2282 },
2283
f02b3f75
AE
2284 /**
2285 * Executes deletion.
2286 *
2287 * @param string action
2288 * @param object parameters
2289 */
2290 _execute: function(action, parameters) {
2291 if (action === 'cancel') {
2292 return;
2293 }
158bd3ca 2294
4dbe4dc2 2295 WCF.LoadingOverlayHandler.updateIcon(parameters.target);
f02b3f75 2296 this._sendRequest(parameters.target);
158bd3ca
TD
2297 },
2298
ca42ad27
MK
2299 /**
2300 * Sends the request
2301 *
2302 * @param jQuery object
2303 */
158bd3ca
TD
2304 _sendRequest: function(object) {
2305 this.proxy.setOption('data', {
2306 actionName: 'delete',
d371330f 2307 className: this._className,
f005c2c9 2308 interfaceName: 'wcf\\data\\IDeleteAction',
158bd3ca
TD
2309 objectIDs: [ $(object).data('objectID') ]
2310 });
2311
2312 this.proxy.sendRequest();
2313 },
2314
2315 /**
2316 * Deletes items from containers.
2317 *
2318 * @param object data
2319 * @param string textStatus
2320 * @param object jqXHR
2321 */
2322 _success: function(data, textStatus, jqXHR) {
da27d58a
MS
2323 this.triggerEffect(data.objectIDs);
2324 },
2325
2326 /**
2327 * Triggers the delete effect for the objects with the given ids.
2328 *
2329 * @param array objectIDs
2330 */
2331 triggerEffect: function(objectIDs) {
d371330f
AE
2332 for (var $index in this._containers) {
2333 var $container = $('#' + this._containers[$index]);
02a8ca7a 2334 if (WCF.inArray($container.find(this._buttonSelector).data('objectID'), objectIDs)) {
90a29960
MS
2335 var self = this;
2336 $container.wcfBlindOut('up',function() {
2337 $(this).remove();
2338 self._containers.splice(self._containers.indexOf($(this).wcfIdentify()), 1);
2339 self._didTriggerEffect($(this));
2340 });
158bd3ca 2341 }
d371330f 2342 }
158bd3ca 2343 }
5ffc72b6 2344});
158bd3ca 2345
8a52f0a4
MS
2346/**
2347 * Basic implementation for deletion of nested elements.
2348 *
2349 * The implementation requires the nested elements to be grouped as numbered lists
2350 * (ol lists). The child elements of the deleted elements are moved to the parent
2351 * element of the deleted element.
2352 *
2353 * @see WCF.Action.Delete
2354 */
2355WCF.Action.NestedDelete = WCF.Action.Delete.extend({
2356 /**
2357 * @see WCF.Action.Delete.triggerEffect()
2358 */
2359 triggerEffect: function(objectIDs) {
2360 for (var $index in this._containers) {
2361 var $container = $('#' + this._containers[$index]);
2362 if (WCF.inArray($container.find(this._buttonSelector).data('objectID'), objectIDs)) {
90a29960
MS
2363 // move children up
2364 if ($container.has('ol').has('li').length) {
8a52f0a4
MS
2365 if ($container.is(':only-child')) {
2366 $container.parent().replaceWith($container.find('> ol'));
2367 }
2368 else {
2369 $container.replaceWith($container.find('> ol > li'));
2370 }
90a29960
MS
2371
2372 this._containers.splice(this._containers.indexOf($container.wcfIdentify()), 1);
2373 this._didTriggerEffect($container);
8a52f0a4
MS
2374 }
2375 else {
90a29960
MS
2376 var self = this;
2377 $container.wcfBlindOut('up', function() {
2378 $(this).remove();
2379 self._containers.splice(self._containers.indexOf($(this).wcfIdentify()), 1);
2380 self._didTriggerEffect($(this));
2381 });
8a52f0a4
MS
2382 }
2383 }
2384 }
2385 }
2386});
2387
158bd3ca
TD
2388/**
2389 * Basic implementation for AJAXProxy-based toggle actions.
2390 *
2391 * @param string className
2392 * @param jQuery containerList
9d4465dd 2393 * @param string buttonSelector
158bd3ca 2394 */
5ffc72b6 2395WCF.Action.Toggle = Class.extend({
9d4465dd
AE
2396 /**
2397 * toogle button selector
2398 * @var string
2399 */
2400 _buttonSelector: '.jsToggleButton',
2401
5dd8bc73
MK
2402 /**
2403 * action class name
2404 * @var string
2405 */
2406 _className: '',
2407
2408 /**
2409 * container selector
2410 * @var string
2411 */
2412 _containerSelector: '',
2413
2414 /**
2415 * list of known container ids
2416 * @var array<string>
2417 */
2418 _containers: [ ],
2419
158bd3ca
TD
2420 /**
2421 * Initializes 'toggle'-Proxy
2422 *
2423 * @param string className
5dd8bc73 2424 * @param string containerSelector
9d4465dd 2425 * @param string buttonSelector
158bd3ca 2426 */
9d4465dd 2427 init: function(className, containerSelector, buttonSelector) {
5dd8bc73
MK
2428 this._containerSelector = containerSelector;
2429 this._className = className;
9d4465dd 2430 this._buttonSelector = (buttonSelector) ? buttonSelector : '.jsToggleButton';
5f6f59eb 2431 this._containers = [ ];
32a9e8a4 2432
158bd3ca
TD
2433 // initialize proxy
2434 var options = {
2435 success: $.proxy(this._success, this)
2436 };
2437 this.proxy = new WCF.Action.Proxy(options);
2438
2439 // bind event listener
46fc9d49
MK
2440 this._initElements();
2441 WCF.DOMNodeInsertedHandler.addCallback('WCF.Action.Toggle' + this._className.hashCode(), $.proxy(this._initElements, this));
5dd8bc73
MK
2442 },
2443
2444 /**
2445 * Initializes available element containers.
2446 */
2447 _initElements: function() {
2448 $(this._containerSelector).each($.proxy(function(index, container) {
2449 var $container = $(container);
2450 var $containerID = $container.wcfIdentify();
2451
2452 if (!WCF.inArray($containerID, this._containers)) {
2453 this._containers.push($containerID);
9d4465dd 2454 $container.find(this._buttonSelector).click($.proxy(this._click, this));
5dd8bc73 2455 }
158bd3ca
TD
2456 }, this));
2457 },
2458
2459 /**
2460 * Sends AJAX request.
2461 *
2462 * @param object event
2463 */
2464 _click: function(event) {
fc8ba59a 2465 var $target = $(event.currentTarget);
b437bee4 2466 event.preventDefault();
fc8ba59a
MK
2467
2468 if ($target.data('confirmMessage')) {
2469 WCF.System.Confirmation.show($target.data('confirmMessage'), $.proxy(this._execute, this), { target: $target });
2470 }
2471 else {
4dbe4dc2 2472 WCF.LoadingOverlayHandler.updateIcon($target);
fc8ba59a
MK
2473 this._sendRequest($target);
2474 }
2475 },
2476
2477 /**
2478 * Executes toggeling.
2479 *
2480 * @param string action
2481 * @param object parameters
2482 */
2483 _execute: function(action, parameters) {
2484 if (action === 'cancel') {
2485 return;
2486 }
2487
4dbe4dc2 2488 WCF.LoadingOverlayHandler.updateIcon(parameters.target);
fc8ba59a
MK
2489 this._sendRequest(parameters.target);
2490 },
2491
2492 _sendRequest: function(object) {
158bd3ca
TD
2493 this.proxy.setOption('data', {
2494 actionName: 'toggle',
5dd8bc73 2495 className: this._className,
f005c2c9 2496 interfaceName: 'wcf\\data\\IToggleAction',
fc8ba59a 2497 objectIDs: [ $(object).data('objectID') ]
158bd3ca
TD
2498 });
2499
2500 this.proxy.sendRequest();
2501 },
2502
2503 /**
2504 * Toggles status icons.
2505 *
2506 * @param object data
2507 * @param string textStatus
2508 * @param object jqXHR
2509 */
2510 _success: function(data, textStatus, jqXHR) {
da27d58a
MS
2511 this.triggerEffect(data.objectIDs);
2512 },
2513
2514 /**
2515 * Triggers the toggle effect for the objects with the given ids.
2516 *
2517 * @param array objectIDs
2518 */
2519 triggerEffect: function(objectIDs) {
5dd8bc73
MK
2520 for (var $index in this._containers) {
2521 var $container = $('#' + this._containers[$index]);
9d4465dd 2522 var $toggleButton = $container.find(this._buttonSelector);
da27d58a 2523 if (WCF.inArray($toggleButton.data('objectID'), objectIDs)) {
f6e4854a
MK
2524 $container.wcfHighlight();
2525 this._toggleButton($container, $toggleButton);
158bd3ca 2526 }
5dd8bc73 2527 }
f6e4854a
MK
2528 },
2529
2530 /**
2531 * Tiggers the toggle effect on a button
2532 *
2533 * @param jQuery $container
2534 * @param jQuery $toggleButton
2535 */
2536 _toggleButton: function($container, $toggleButton) {
2537 // toggle icon source
4dbe4dc2 2538 WCF.LoadingOverlayHandler.updateIcon($toggleButton, false);
d3bd4037
MW
2539 if ($toggleButton.hasClass('icon-check-empty')) {
2540 $toggleButton.removeClass('icon-check-empty').addClass('icon-check');
4aa6daef 2541 $newTitle = ($toggleButton.data('disableTitle') ? $toggleButton.data('disableTitle') : WCF.Language.get('wcf.global.button.disable'));
556973c1
MW
2542 $toggleButton.attr('title', $newTitle);
2543 }
2544 else {
d3bd4037 2545 $toggleButton.removeClass('icon-check').addClass('icon-check-empty');
4aa6daef 2546 $newTitle = ($toggleButton.data('enableTitle') ? $toggleButton.data('enableTitle') : WCF.Language.get('wcf.global.button.enable'));
556973c1
MW
2547 $toggleButton.attr('title', $newTitle);
2548 }
2549
f6e4854a
MK
2550 // toggle css class
2551 $container.toggleClass('disabled');
158bd3ca 2552 }
5ffc72b6 2553});
158bd3ca 2554
aa4fb64e
AE
2555/**
2556 * Executes provided callback if scroll threshold is reached. Usuable to determine
2557 * if user reached the bottom of an element to load new elements on the fly.
2558 *
2559 * If you do not provide a value for 'reference' and 'target' it will assume you're
2560 * monitoring page scrolls, otherwise a valid jQuery selector must be provided for both.
2561 *
2562 * @param integer threshold
2563 * @param object callback
2564 * @param string reference
2565 * @param string target
2566 */
2567WCF.Action.Scroll = Class.extend({
2568 /**
2569 * callback used once threshold is reached
2570 * @var object
2571 */
2572 _callback: null,
2573
2574 /**
2575 * reference object
2576 * @var jQuery
2577 */
2578 _reference: null,
2579
2580 /**
2581 * target object
2582 * @var jQuery
2583 */
2584 _target: null,
2585
2586 /**
2587 * threshold value
2588 * @var integer
2589 */
2590 _threshold: 0,
2591
2592 /**
2593 * Initializes a new WCF.Action.Scroll object.
2594 *
2595 * @param integer threshold
2596 * @param object callback
2597 * @param string reference
2598 * @param string target
2599 */
2600 init: function(threshold, callback, reference, target) {
2601 this._threshold = parseInt(threshold);
2602 if (this._threshold === 0) {
2603 console.debug("[WCF.Action.Scroll] Given threshold is invalid, aborting.");
2604 return;
2605 }
2606
2607 if ($.isFunction(callback)) this._callback = callback;
2608 if (this._callback === null) {
2609 console.debug("[WCF.Action.Scroll] Given callback is invalid, aborting.");
2610 return;
2611 }
2612
2613 // bind element references
2614 this._reference = $((reference) ? reference : window);
2615 this._target = $((target) ? target : document);
2616
2617 // watch for scroll event
2618 this.start();
5cc5eff0
AE
2619
2620 // check if browser navigated back and jumped to offset before JavaScript was loaded
2621 this._scroll();
aa4fb64e
AE
2622 },
2623
2624 /**
2625 * Calculates if threshold is reached and notifies callback.
2626 */
2627 _scroll: function() {
2628 var $targetHeight = this._target.height();
2629 var $topOffset = this._reference.scrollTop();
2630 var $referenceHeight = this._reference.height();
2631
2632 // calculate if defined threshold is visible
2633 if (($targetHeight - ($referenceHeight + $topOffset)) < this._threshold) {
2634 this._callback(this);
2635 }
2636 },
2637
2638 /**
2639 * Enables scroll monitoring, may be used to resume.
2640 */
2641 start: function() {
2642 this._reference.on('scroll', $.proxy(this._scroll, this));
221fce41 2643 },
aa4fb64e
AE
2644
2645 /**
2646 * Disables scroll monitoring, e.g. no more elements loadable.
2647 */
2648 stop: function() {
2649 this._reference.off('scroll');
2650 }
2651});
2652
158bd3ca
TD
2653/**
2654 * Namespace for date-related functions.
2655 */
2656WCF.Date = {};
2657
81f55d8f
AE
2658/**
2659 * Provides a date picker for date input fields.
2660 */
2661WCF.Date.Picker = {
645231ef
AE
2662 /**
2663 * date format
2664 * @var string
2665 */
6af5fef6
DR
2666 _dateFormat: 'yy-mm-dd',
2667
b418da85
AE
2668 /**
2669 * time format
2670 * @var string
2671 */
2672 _timeFormat: 'g:ia',
2673
81f55d8f
AE
2674 /**
2675 * Initializes the jQuery UI based date picker.
2676 */
2677 init: function() {
b418da85
AE
2678 // ignore error 'unexpected literal' error; this might be not the best approach
2679 // to fix this problem, but since the date is properly processed anyway, we can
2680 // simply continue :) - Alex
2681 var $__log = $.timepicker.log;
2682 $.timepicker.log = function(error) {
65dd3bae 2683 if (error.indexOf('Error parsing the date/time string: Unexpected literal at position') == -1 && error.indexOf('Error parsing the date/time string: Unknown name at position') == -1) {
b418da85
AE
2684 $__log(error);
2685 }
2686 };
2687
6af5fef6 2688 this._convertDateFormat();
c5286020 2689 this._initDatePicker();
c5286020
AE
2690 WCF.DOMNodeInsertedHandler.addCallback('WCF.Date.Picker', $.proxy(this._initDatePicker, this));
2691 },
2692
6af5fef6
DR
2693 /**
2694 * Convert PHPs date() format to jQuery UIs date picker format.
2695 */
2696 _convertDateFormat: function() {
6af5fef6
DR
2697 // replacement table
2698 // format of PHP date() => format of jQuery UI date picker
2699 //
2700 // No equivalence in PHP date():
2701 // oo day of the year (three digit)
2702 // ! Windows ticks (100ns since 01/01/0001)
2703 //
2704 // No equivalence in jQuery UI date picker:
2705 // N ISO-8601 numeric representation of the day of the week
6af5fef6
DR
2706 // w Numeric representation of the day of the week
2707 // W ISO-8601 week number of year, weeks starting on Monday
2708 // t Number of days in the given month
2709 // L Whether it's a leap year
d83154c3 2710 var $replacementTable = {
b418da85 2711 // time
65dd3bae
AE
2712 'a': 'tt',
2713 'A': 'TT',
b418da85
AE
2714 'g': 'h',
2715 'G': 'H',
2716 'h': 'hh',
2717 'H': 'HH',
2718 'i': 'mm',
2719 's': 'ss',
2720 'u': 'l',
2721
6af5fef6
DR
2722 // day
2723 'd': 'dd',
2724 'D': 'D',
2725 'j': 'd',
2726 'l': 'DD',
2727 'z': 'o',
5e4cf40e 2728 'S': '', // English ordinal suffix for the day of the month, 2 characters, will be discarded
6af5fef6
DR
2729
2730 // month
2731 'F': 'MM',
2732 'm': 'mm',
2733 'M': 'M',
2734 'n': 'm',
2735
2736 // year
2737 'o': 'yy',
2738 'Y': 'yy',
2739 'y': 'y',
2740
2741 // timestamp
2742 'U': '@'
2743 };
2744
2745 // do the actual replacement
2746 // this is not perfect, but a basic implementation and should work in 99% of the cases
5e4cf40e 2747 this._dateFormat = WCF.Language.get('wcf.date.dateFormat').replace(/([^dDjlzSFmMnoYyU\\]*(?:\\.[^dDjlzSFmMnoYyU\\]*)*)([dDjlzSFmMnoYyU])/g, function(match, part1, part2, offset, string) {
d83154c3
AE
2748 for (var $key in $replacementTable) {
2749 if (part2 == $key) {
2750 part2 = $replacementTable[$key];
6af5fef6 2751 }
d83154c3
AE
2752 }
2753
6af5fef6
DR
2754 return part1 + part2;
2755 });
b418da85
AE
2756
2757 this._timeFormat = WCF.Language.get('wcf.date.timeFormat').replace(/([^aAgGhHisu\\]*(?:\\.[^aAgGhHisu\\]*)*)([aAgGhHisu])/g, function(match, part1, part2, offset, string) {
2758 for (var $key in $replacementTable) {
2759 if (part2 == $key) {
2760 part2 = $replacementTable[$key];
2761 }
2762 }
2763
2764 return part1 + part2;
2765 });
6af5fef6
DR
2766 },
2767
c5286020
AE
2768 /**
2769 * Initializes the date picker for valid fields.
2770 */
2771 _initDatePicker: function() {
b9e59fc3 2772 $('input[type=date]:not(.jsDatePicker), input[type=datetime]:not(.jsDatePicker)').each($.proxy(function(index, input) {
4209cfa0 2773 var $input = $(input);
d83154c3 2774 var $inputName = $input.prop('name');
b9e59fc3
MS
2775 var $inputValue = $input.val(); // should be Y-m-d (H:i:s), must be interpretable by Date
2776
2777 var $hasTime = $input.attr('type') == 'datetime';
81f55d8f 2778
4209cfa0
DR
2779 // update $input
2780 $input.prop('type', 'text').addClass('jsDatePicker');
2781
86db109f
MW
2782 // set placeholder
2783 if ($input.data('placeholder')) $input.attr('placeholder', $input.data('placeholder'));
2784
4209cfa0 2785 // insert a hidden element representing the actual date
d83154c3
AE
2786 $input.removeAttr('name');
2787 $input.before('<input type="hidden" id="' + $input.wcfIdentify() + 'DatePicker" name="' + $inputName + '" value="' + $inputValue + '" />');
81f55d8f 2788
b9e59fc3
MS
2789 // max- and mindate
2790 var $maxDate = $input.attr('max') ? new Date($input.attr('max').replace(' ', 'T')) : null;
2791 var $minDate = $input.attr('min') ? new Date($input.attr('min').replace(' ', 'T')) : null;
2792
4209cfa0 2793 // init date picker
b9e59fc3 2794 $options = {
d83154c3
AE
2795 altField: '#' + $input.wcfIdentify() + 'DatePicker',
2796 altFormat: 'yy-mm-dd', // PHPs strtotime() understands this best
5a1293b4
AE
2797 beforeShow: function(input, instance) {
2798 // dirty hack to force opening below the input
2799 setTimeout(function() {
2800 instance.dpDiv.position({
2801 my: 'left top',
2802 at: 'left bottom',
2803 collision: 'none',
2804 of: input
2805 });
2806 }, 1);
2807 },
81f55d8f
AE
2808 changeMonth: true,
2809 changeYear: true,
6af5fef6 2810 dateFormat: this._dateFormat,
6af5fef6
DR
2811 dayNames: WCF.Language.get('__days'),
2812 dayNamesMin: WCF.Language.get('__daysShort'),
2813 dayNamesShort: WCF.Language.get('__daysShort'),
9f8d4e52 2814 firstDay: parseInt(WCF.Language.get('wcf.date.firstDayOfTheWeek')) || 0,
b9e59fc3
MS
2815 isRTL: WCF.Language.get('wcf.global.pageDirection') == 'rtl',
2816 maxDate: $maxDate,
2817 minDate: $minDate,
6af5fef6 2818 monthNames: WCF.Language.get('__months'),
d83154c3 2819 monthNamesShort: WCF.Language.get('__monthsShort'),
b9e59fc3 2820 showButtonPanel: false,
25bed976
TD
2821 onClose: function(dateText, datePicker) {
2822 // clear altField when datepicker is cleared
2823 if (dateText == '') {
2824 $(datePicker.settings["altField"]).val(dateText);
2825 }
b9e59fc3
MS
2826 },
2827 showOtherMonths: true,
2828 yearRange: ($input.hasClass('birthday') ? '-100:+0' : '1900:2038')
2829 };
4209cfa0 2830
b9e59fc3
MS
2831 if ($hasTime) {
2832 // drop the seconds
2833 if (/[0-9]{2}:[0-9]{2}:[0-9]{2}$/.test($inputValue)) {
2834 $inputValue = $inputValue.replace(/:[0-9]{2}$/, '');
2835 $input.val($inputValue);
2836 }
2837 $inputValue = $inputValue.replace(' ', 'T');
2838
2839 $options = $.extend($options, {
2840 altFieldTimeOnly: false,
2841 altTimeFormat: 'HH:mm',
2842 controlType: 'select',
2843 hourText: WCF.Language.get('wcf.date.hour'),
2844 minuteText: WCF.Language.get('wcf.date.minute'),
2845 showTime: false,
2846 timeFormat: this._timeFormat,
2847 yearRange: ($input.hasClass('birthday') ? '-100:+0' : '1900:2038')
2848 });
645231ef 2849 }
4209cfa0 2850
b9e59fc3
MS
2851 if ($hasTime) {
2852 $input.datetimepicker($options);
2853 }
2854 else {
2855 $input.datepicker($options);
b418da85 2856 }
b418da85
AE
2857
2858 // format default date
2859 if ($inputValue) {
b9e59fc3 2860 $input.datepicker('setDate', $inputValue);
b418da85
AE
2861 }
2862
2863 // bug workaround: setDate creates the widget but unfortunately doesn't hide it...
2864 $input.datepicker('widget').hide();
2865 }, this));
81f55d8f
AE
2866 }
2867};
2868
158bd3ca
TD
2869/**
2870 * Provides utility functions for date operations.
2871 */
2872WCF.Date.Util = {
2873 /**
2874 * Returns UTC timestamp, if date is not given, current time will be used.
2875 *
2876 * @param Date date
2877 * @return integer
2878 */
2879 gmdate: function(date) {
2880 var $date = (date) ? date : new Date();
2881
2882 return Math.round(Date.UTC(
2883 $date.getUTCFullYear(),
2884 $date.getUTCMonth(),
2885 $date.getUTCDay(),
2886 $date.getUTCHours(),
2887 $date.getUTCMinutes(),
2888 $date.getUTCSeconds()
2889 ) / 1000);
2890 },
2891
2892 /**
2893 * Returns a Date object with precise offset (including timezone and local timezone).
88ff183f 2894 * Parameters timestamp and offset must be in miliseconds!
158bd3ca
TD
2895 *
2896 * @param integer timestamp
2897 * @param integer offset
2898 * @return Date
2899 */
2900 getTimezoneDate: function(timestamp, offset) {
2901 var $date = new Date(timestamp);
88ff183f 2902 var $localOffset = $date.getTimezoneOffset() * 60000;
158bd3ca 2903
88ff183f 2904 return new Date((timestamp + $localOffset + offset));
158bd3ca
TD
2905 }
2906};
2907
2908/**
2909 * Handles relative time designations.
2910 */
81f55d8f 2911WCF.Date.Time = Class.extend({
9cf417b1
MS
2912 /**
2913 * Date of current timestamp
2914 * @var Date
2915 */
2916 _date: 0,
2917
8574cbfd
AE
2918 /**
2919 * list of time elements
2920 * @var jQuery
2921 */
2922 _elements: null,
2923
2924 /**
2925 * difference between server and local time
2926 * @var integer
2927 */
2928 _offset: null,
2929
2930 /**
2931 * current timestamp
2932 * @var integer
2933 */
2934 _timestamp: 0,
2935
158bd3ca
TD
2936 /**
2937 * Initializes relative datetimes.
2938 */
2939 init: function() {
8574cbfd
AE
2940 this._elements = $('time.datetime');
2941 this._offset = null;
2942 this._timestamp = 0;
158bd3ca
TD
2943
2944 // calculate relative datetime on init
2945 this._refresh();
2946
2947 // re-calculate relative datetime every minute
2948 new WCF.PeriodicalExecuter($.proxy(this._refresh, this), 60000);
9f959ced 2949
9c1e5045
AE
2950 // bind dom node inserted listener
2951 WCF.DOMNodeInsertedHandler.addCallback('WCF.Date.Time', $.proxy(this._domNodeInserted, this));
2952 },
9f959ced 2953
9c1e5045
AE
2954 /**
2955 * Updates element collection once a DOM node was inserted.
2956 */
2957 _domNodeInserted: function() {
8574cbfd 2958 this._elements = $('time.datetime');
9c1e5045 2959 this._refresh();
158bd3ca
TD
2960 },
2961
2962 /**
2963 * Refreshes relative datetime for each element.
2964 */
2965 _refresh: function() {
9cf417b1
MS
2966 this._date = new Date();
2967 this._timestamp = (this._date.getTime() - this._date.getMilliseconds()) / 1000;
8574cbfd
AE
2968 if (this._offset === null) {
2969 this._offset = this._timestamp - TIME_NOW;
2970 }
158bd3ca 2971
8574cbfd 2972 this._elements.each($.proxy(this._refreshElement, this));
158bd3ca
TD
2973 },
2974
2975 /**
2976 * Refreshes relative datetime for current element.
2977 *
2978 * @param integer index
2979 * @param object element
2980 */
2981 _refreshElement: function(index, element) {
8574cbfd
AE
2982 var $element = $(element);
2983
2984 if (!$element.attr('title')) {
2985 $element.attr('title', $element.text());
158bd3ca
TD
2986 }
2987
8574cbfd
AE
2988 var $timestamp = $element.data('timestamp') + this._offset;
2989 var $date = $element.data('date');
2990 var $time = $element.data('time');
2991 var $offset = $element.data('offset');
158bd3ca 2992
3ce1cb95
MW
2993 // skip for future dates
2994 if ($element.data('isFutureDate')) return;
2995
270599e2
MW
2996 // timestamp is less than 60 seconds ago
2997 if ($timestamp >= this._timestamp || this._timestamp < ($timestamp + 60)) {
2998 $element.text(WCF.Language.get('wcf.date.relative.now'));
2999 }
158bd3ca 3000 // timestamp is less than 60 minutes ago (display 1 hour ago rather than 60 minutes ago)
270599e2 3001 else if (this._timestamp < ($timestamp + 3540)) {
69948aa5 3002 var $minutes = Math.max(Math.round((this._timestamp - $timestamp) / 60), 1);
fef32fb1 3003 $element.text(WCF.Language.get('wcf.date.relative.minutes', { minutes: $minutes }));
158bd3ca
TD
3004 }
3005 // timestamp is less than 24 hours ago
8574cbfd
AE
3006 else if (this._timestamp < ($timestamp + 86400)) {
3007 var $hours = Math.round((this._timestamp - $timestamp) / 3600);
fef32fb1 3008 $element.text(WCF.Language.get('wcf.date.relative.hours', { hours: $hours }));
158bd3ca 3009 }
54e44c71
AE
3010 // timestamp is less than 6 days ago
3011 else if (this._timestamp < ($timestamp + 518400)) {
9cf417b1
MS
3012 var $midnight = new Date(this._date.getFullYear(), this._date.getMonth(), this._date.getDate());
3013 var $days = Math.ceil(($midnight / 1000 - $timestamp) / 86400);
9f959ced 3014
158bd3ca 3015 // get day of week
88ff183f 3016 var $dateObj = WCF.Date.Util.getTimezoneDate(($timestamp * 1000), $offset * 1000);
158bd3ca 3017 var $dow = $dateObj.getDay();
fef32fb1 3018 var $day = WCF.Language.get('__days')[$dow];
158bd3ca 3019
fef32fb1 3020 $element.text(WCF.Language.get('wcf.date.relative.pastDays', { days: $days, day: $day, time: $time }));
158bd3ca
TD
3021 }
3022 // timestamp is between ~700 million years BC and last week
3023 else {
c02c7d58 3024 var $string = WCF.Language.get('wcf.date.shortDateTimeFormat');
8574cbfd 3025 $element.text($string.replace(/\%date\%/, $date).replace(/\%time\%/, $time));
158bd3ca
TD
3026 }
3027 }
81f55d8f 3028});
158bd3ca
TD
3029
3030/**
3031 * Hash-like dictionary. Based upon idead from Prototype's hash
3032 *
3033 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/hash.js
3034 */
39e27190 3035WCF.Dictionary = Class.extend({
264e3f79
AE
3036 /**
3037 * list of variables
3038 * @var object
3039 */
3040 _variables: { },
3041
158bd3ca
TD
3042 /**
3043 * Initializes a new dictionary.
3044 */
83428b7f
AE
3045 init: function() {
3046 this._variables = { };
3047 },
158bd3ca
TD
3048
3049 /**
3050 * Adds an entry.
3051 *
3052 * @param string key
3053 * @param mixed value
3054 */
3055 add: function(key, value) {
264e3f79 3056 this._variables[key] = value;
158bd3ca
TD
3057 },
3058
3059 /**
3060 * Adds a traditional object to current dataset.
3061 *
3062 * @param object object
3063 */
3064 addObject: function(object) {
3065 for (var $key in object) {
3066 this.add($key, object[$key]);
3067 }
3068 },
3069
3070 /**
3071 * Adds a dictionary to current dataset.
3072 *
3073 * @param object dictionary
3074 */
3075 addDictionary: function(dictionary) {
3076 dictionary.each($.proxy(function(pair) {
3077 this.add(pair.key, pair.value);
3078 }, this));
3079 },
3080
3081 /**
3082 * Retrieves the value of an entry or returns null if key is not found.
3083 *
3084 * @param string key
3085 * @returns mixed
3086 */
3087 get: function(key) {
3088 if (this.isset(key)) {
264e3f79 3089 return this._variables[key];
158bd3ca
TD
3090 }
3091
3092 return null;
3093 },
3094
3095 /**
3096 * Returns true if given key is a valid entry.
3097 *
3098 * @param string key
3099 */
3100 isset: function(key) {
264e3f79 3101 return this._variables.hasOwnProperty(key);
158bd3ca
TD
3102 },
3103
3104 /**
3105 * Removes an entry.
3106 *
3107 * @param string key
3108 */
3109 remove: function(key) {
264e3f79 3110 delete this._variables[key];
158bd3ca
TD
3111 },
3112
3113 /**
3114 * Iterates through dictionary.
3115 *
3116 * Usage:
3117 * var $hash = new WCF.Dictionary();
3118 * $hash.add('foo', 'bar');
3119 * $hash.each(function(pair) {
3120 * // alerts: foo = bar
3121 * alert(pair.key + ' = ' + pair.value);
3122 * });
3123 *
3124 * @param function callback
3125 */
3126 each: function(callback) {
3127 if (!$.isFunction(callback)) {
3128 return;
3129 }
3130
264e3f79
AE
3131 for (var $key in this._variables) {
3132 var $value = this._variables[$key];
158bd3ca
TD
3133 var $pair = {
3134 key: $key,
3135 value: $value
3136 };
3137
3138 callback($pair);
3139 }
264e3f79
AE
3140 },
3141
3142 /**
3143 * Returns the amount of items.
3144 *
3145 * @return integer
3146 */
3147 count: function() {
3148 return $.getLength(this._variables);
3149 },
3150
3151 /**
28410a97 3152 * Returns true if dictionary is empty.
264e3f79
AE
3153 *
3154 * @return integer
3155 */
3156 isEmpty: function() {
3157 return !this.count();
158bd3ca 3158 }
39e27190 3159});
158bd3ca
TD
3160
3161/**
3162 * Global language storage.
3163 *
3164 * @see WCF.Dictionary
3165 */
3166WCF.Language = {
3167 _variables: new WCF.Dictionary(),
3168
3169 /**
f2e420cc 3170 * @see WCF.Dictionary.add()
158bd3ca
TD
3171 */
3172 add: function(key, value) {
3173 this._variables.add(key, value);
3174 },
3175
3176 /**
3177 * @see WCF.Dictionary.addObject()
3178 */
3179 addObject: function(object) {
3180 this._variables.addObject(object);
3181 },
3182
3183 /**
3184 * Retrieves a variable.
3185 *
3186 * @param string key
3187 * @return mixed
3188 */
12876b18
TD
3189 get: function(key, parameters) {
3190 // initialize parameters with an empty object
66a11454 3191 if (parameters == null) var parameters = { };
12876b18
TD
3192
3193 var value = this._variables.get(key);
3194
8a236dff
TD
3195 if (value === null) {
3196 // return key again
3197 return key;
3198 }
3199 else if (typeof value === 'string') {
12876b18
TD
3200 // transform strings into template and try to refetch
3201 this.add(key, new WCF.Template(value));
3202 return this.get(key, parameters);
3203 }
66a11454 3204 else if (typeof value.fetch === 'function') {
12876b18
TD
3205 // evaluate templates
3206 value = value.fetch(parameters);
3207 }
3208
3209 return value;
158bd3ca
TD
3210 }
3211};
f2e420cc 3212
2b4d2743
AE
3213/**
3214 * Handles multiple language input fields.
3215 *
3216 * @param string elementID
3217 * @param boolean forceSelection
3218 * @param object values
3219 * @param object availableLanguages
3220 */
7382b20f 3221WCF.MultipleLanguageInput = Class.extend({
2b4d2743
AE
3222 /**
3223 * list of available languages
3224 * @var object
3225 */
3226 _availableLanguages: {},
9f959ced 3227
3ef8dee9
AE
3228 /**
3229 * button element
3230 * @var jQuery
3231 */
3232 _button: null,
3233
2b4d2743
AE
3234 /**
3235 * initialization state
3236 * @var boolean
3237 */
3238 _didInit: false,
9f959ced 3239
2b4d2743
AE
3240 /**
3241 * target input element
3242 * @var jQuery
3243 */
3244 _element: null,
38047181
AE
3245
3246 /**
3247 * true, if data was entered after initialization
3248 * @var boolean
3249 */
3250 _insertedDataAfterInit: false,
9f959ced 3251
2b4d2743
AE
3252 /**
3253 * enables multiple language ability
3254 * @var boolean
3255 */
3256 _isEnabled: false,
9f959ced 3257
2b4d2743
AE
3258 /**
3259 * enforce multiple language ability
3260 * @var boolean
3261 */
3262 _forceSelection: false,
9f959ced 3263
2b4d2743
AE
3264 /**
3265 * currently active language id
3266 * @var integer
3267 */
3268 _languageID: 0,
9f959ced 3269
2b4d2743
AE
3270 /**
3271 * language selection list
3272 * @var jQuery
3273 */
3274 _list: null,
9f959ced 3275
2b4d2743
AE
3276 /**
3277 * list of language values on init
3278 * @var object
3279 */
3280 _values: null,
9f959ced 3281
2b4d2743
AE
3282 /**
3283 * Initializes multiple language ability for given element id.
3284 *
3285 * @param integer elementID
3286 * @param boolean forceSelection
3287 * @param boolean isEnabled
3288 * @param object values
3289 * @param object availableLanguages
3290 */
3291 init: function(elementID, forceSelection, values, availableLanguages) {
3ef8dee9 3292 this._button = null;
2b4d2743
AE
3293 this._element = $('#' + $.wcfEscapeID(elementID));
3294 this._forceSelection = forceSelection;
3295 this._values = values;
3296 this._availableLanguages = availableLanguages;
3297
cc7db7fa
AE
3298 // unescape values
3299 if ($.getLength(this._values)) {
3300 for (var $key in this._values) {
3301 this._values[$key] = WCF.String.unescapeHTML(this._values[$key]);
3302 }
3303 }
3304
2b4d2743
AE
3305 // default to current user language
3306 this._languageID = LANGUAGE_ID;
3307 if (this._element.length == 0) {
3308 console.debug("[WCF.MultipleLanguageInput] element id '" + elementID + "' is unknown");
3309 return;
3310 }
3311
3312 // build selection handler
3313 var $enableOnInit = ($.getLength(this._values) > 0) ? true : false;
38047181 3314 this._insertedDataAfterInit = $enableOnInit;
2b4d2743
AE
3315 this._prepareElement($enableOnInit);
3316
3317 // listen for submit event
3318 this._element.parents('form').submit($.proxy(this._submit, this));
9f959ced 3319
2b4d2743
AE
3320 this._didInit = true;
3321 },
9f959ced 3322
2b4d2743
AE
3323 /**
3324 * Builds language handler.
3325 *
3326 * @param boolean enableOnInit
3327 */
3328 _prepareElement: function(enableOnInit) {
184a8d6d 3329 this._element.wrap('<div class="dropdown preInput" />');
2b4d2743 3330 var $wrapper = this._element.parent();
3ef8dee9 3331 this._button = $('<p class="button dropdownToggle"><span>' + WCF.Language.get('wcf.global.button.disabledI18n') + '</span></p>').prependTo($wrapper);
184a8d6d 3332
7382b20f 3333 // insert list
3ef8dee9 3334 this._list = $('<ul class="dropdownMenu"></ul>').insertAfter(this._button);
7382b20f 3335
184a8d6d 3336 // add a special class if next item is a textarea
3ef8dee9
AE
3337 if (this._button.nextAll('textarea').length) {
3338 this._button.addClass('dropdownCaptionTextarea');
184a8d6d
AE
3339 }
3340 else {
3ef8dee9 3341 this._button.addClass('dropdownCaption');
1b049030 3342 }
184a8d6d
AE
3343
3344 // insert available languages
3345 for (var $languageID in this._availableLanguages) {
3346 $('<li><span>' + this._availableLanguages[$languageID] + '</span></li>').data('languageID', $languageID).click($.proxy(this._changeLanguage, this)).appendTo(this._list);
3347 }
9f959ced 3348
184a8d6d
AE
3349 // disable language input
3350 if (!this._forceSelection) {
3351 $('<li class="dropdownDivider" />').appendTo(this._list);
3352 $('<li><span>' + WCF.Language.get('wcf.global.button.disabledI18n') + '</span></li>').click($.proxy(this._disable, this)).appendTo(this._list);
3353 }
2b4d2743 3354
3ef8dee9
AE
3355 WCF.Dropdown.initDropdown(this._button, enableOnInit);
3356
46b44739 3357 if (enableOnInit || this._forceSelection) {
0a7c08e0
AE
3358 this._isEnabled = true;
3359
2b4d2743
AE
3360 // pre-select current language
3361 this._list.children('li').each($.proxy(function(index, listItem) {
3362 var $listItem = $(listItem);
3363 if ($listItem.data('languageID') == this._languageID) {
3364 $listItem.trigger('click');
3365 }
3366 }, this));
3367 }
184a8d6d
AE
3368
3369 WCF.Dropdown.registerCallback($wrapper.wcfIdentify(), $.proxy(this._handleAction, this));
3370 },
3371
3372 /**
3373 * Handles dropdown actions.
3374 *
3ef8dee9 3375 * @param string containerID
184a8d6d
AE
3376 * @param string action
3377 */
3ef8dee9
AE
3378 _handleAction: function(containerID, action) {
3379 if (action === 'open') {
3380 this._enable();
3381 }
3382 else {
184a8d6d
AE
3383 this._closeSelection();
3384 }
2b4d2743 3385 },
9f959ced 3386
2b4d2743
AE
3387 /**
3388 * Enables the language selection or shows the selection if already enabled.
3389 *
3390 * @param object event
3391 */
3392 _enable: function(event) {
3393 if (!this._isEnabled) {
3ef8dee9 3394 var $button = (this._button.is('p')) ? this._button.children('span:eq(0)') : this._button;
2b4d2743 3395 $button.addClass('active');
184a8d6d 3396
2b4d2743
AE
3397 this._isEnabled = true;
3398 }
184a8d6d 3399
2b4d2743
AE
3400 // toggle list
3401 if (this._list.is(':visible')) {
2b4d2743
AE
3402 this._showSelection();
3403 }
2b4d2743 3404 },
9f959ced 3405
2b4d2743
AE
3406 /**
3407 * Shows the language selection.
3408 */
3409 _showSelection: function() {
3410 if (this._isEnabled) {
3411 // display status for each language
3412 this._list.children('li').each($.proxy(function(index, listItem) {
3413 var $listItem = $(listItem);
3414 var $languageID = $listItem.data('languageID');
9f959ced 3415
2b4d2743
AE
3416 if ($languageID) {
3417 if (this._values[$languageID] && this._values[$languageID] != '') {
3418 $listItem.removeClass('missingValue');
3419 }
3420 else {
3421 $listItem.addClass('missingValue');
3422 }
3423 }
3424 }, this));
2b4d2743
AE
3425 }
3426 },
9f959ced 3427
2b4d2743
AE
3428 /**
3429 * Closes the language selection.
3430 */
3431 _closeSelection: function() {
055c009e 3432 this._disable();
2b4d2743 3433 },
9f959ced 3434
2b4d2743
AE
3435 /**
3436 * Changes the currently active language.
3437 *
3438 * @param object event
3439 */
3440 _changeLanguage: function(event) {
184a8d6d 3441 var $button = $(event.currentTarget);
38047181
AE
3442 this._insertedDataAfterInit = true;
3443
2b4d2743
AE
3444 // save current value
3445 if (this._didInit) {
3446 this._values[this._languageID] = this._element.val();
3447 }
38047181 3448
2b4d2743
AE
3449 // set new language
3450 this._languageID = $button.data('languageID');
3451 if (this._values[this._languageID]) {
3452 this._element.val(this._values[this._languageID]);
3453 }
3454 else {
3455 this._element.val('');
3456 }
38047181 3457
2b4d2743
AE
3458 // update marking
3459 this._list.children('li').removeClass('active');
3460 $button.addClass('active');
38047181 3461
2b4d2743 3462 // update label
3ef8dee9 3463 this._button.children('span').addClass('active').text(this._availableLanguages[this._languageID]);
38047181 3464
2b4d2743 3465 // close selection and set focus on input element
8c368f9a 3466 if (this._didInit) {
8c368f9a
MS
3467 this._element.blur().focus();
3468 }
2b4d2743 3469 },
9f959ced 3470
2b4d2743
AE
3471 /**
3472 * Disables language selection for current element.
0bd3dc4b
AE
3473 *
3474 * @param object event
2b4d2743 3475 */
0bd3dc4b 3476 _disable: function(event) {
055c009e
AE
3477 if (event === undefined && this._insertedDataAfterInit) {
3478 event = null;
3479 }
3480
3481 if (this._forceSelection || !this._list || event === null) {
46b44739
AE
3482 return;
3483 }
3484
2b4d2743 3485 // remove active marking
3ef8dee9 3486 this._button.children('span').removeClass('active').text(WCF.Language.get('wcf.global.button.disabledI18n'));
9f959ced 3487
2b4d2743
AE
3488 // update element value
3489 if (this._values[LANGUAGE_ID]) {
3490 this._element.val(this._values[LANGUAGE_ID]);
3491 }
3492 else {
3493 // no value for current language found, proceed with empty input
3494 this._element.val();
3495 }
84481c65 3496
93736c72
MS
3497 if (event) {
3498 this._list.children('li').removeClass('active');
3499 $(event.currentTarget).addClass('active');
3500 }
3501
3a9a460c 3502 this._element.blur().focus();
055c009e 3503 this._insertedDataAfterInit = false;
2b4d2743 3504 this._isEnabled = false;
055c009e 3505 this._values = { };
2b4d2743 3506 },
9f959ced 3507
2b4d2743
AE
3508 /**
3509 * Prepares language variables on before submit.
3510 */
3511 _submit: function() {
3512 // insert hidden form elements on before submit
3513 if (!this._isEnabled) {
055c009e 3514 return 0xDEADBEEF;
2b4d2743 3515 }
9f959ced 3516
2b4d2743
AE
3517 // fetch active value
3518 if (this._languageID) {
3519 this._values[this._languageID] = this._element.val();
3520 }
9f959ced 3521
2b4d2743
AE
3522 var $form = $(this._element.parents('form')[0]);
3523 var $elementID = this._element.wcfIdentify();
9f959ced 3524
4aae8961
AE
3525 for (var $languageID in this._availableLanguages) {
3526 if (this._values[$languageID] === undefined) {
3527 this._values[$languageID] = '';
3528 }
3529
cc7db7fa 3530 $('<input type="hidden" name="' + $elementID + '_i18n[' + $languageID + ']" value="' + WCF.String.escapeHTML(this._values[$languageID]) + '" />').appendTo($form);
2b4d2743 3531 }
9f959ced 3532
2b4d2743
AE
3533 // remove name attribute to prevent conflict with i18n values
3534 this._element.removeAttr('name');
3535 }
7382b20f 3536});
2b4d2743 3537
52560d9e
TD
3538/**
3539 * Number utilities.
3540 */
3541WCF.Number = {
3542 /**
7e969bc7 3543 * Rounds a number to a given number of decimal places. Defaults to 0.
52560d9e
TD
3544 *
3545 * @param number number
7e969bc7 3546 * @param decimalPlaces number of decimal places
52560d9e
TD
3547 * @return number
3548 */
7e969bc7
TD
3549 round: function (number, decimalPlaces) {
3550 decimalPlaces = Math.pow(10, (decimalPlaces || 0));
52560d9e 3551
7e969bc7 3552 return Math.round(number * decimalPlaces) / decimalPlaces;
52560d9e 3553 }
f4126129 3554};
52560d9e 3555
158bd3ca
TD
3556/**
3557 * String utilities.
3558 */
3559WCF.String = {
52560d9e
TD
3560 /**
3561 * Adds thousands separators to a given number.
3562 *
2aebf1f5 3563 * @see http://stackoverflow.com/a/6502556/782822
52560d9e
TD
3564 * @param mixed number
3565 * @return string
3566 */
3567 addThousandsSeparator: function(number) {
955082b0 3568 return String(number).replace(/(^-?\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, '$1' + WCF.Language.get('wcf.global.thousandsSeparator'));
52560d9e
TD
3569 },
3570
158bd3ca
TD
3571 /**
3572 * Escapes special HTML-characters within a string
3573 *
3574 * @param string string
3575 * @return string
3576 */
3577 escapeHTML: function (string) {
bf3df436 3578 return String(string).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
158bd3ca
TD
3579 },
3580
3581 /**
3582 * Escapes a String to work with RegExp.
7b608580 3583 *
158bd3ca
TD
3584 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25
3585 * @param string string
3586 * @return string
3587 */
3588 escapeRegExp: function(string) {
bf3df436 3589 return String(string).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
158bd3ca
TD
3590 },
3591
3592 /**
52560d9e 3593 * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands-separators
158bd3ca 3594 *
52560d9e 3595 * @param mixed number
158bd3ca
TD
3596 * @return string
3597 */
7e969bc7
TD
3598 formatNumeric: function(number, decimalPlaces) {
3599 number = String(WCF.Number.round(number, decimalPlaces || 2));
52560d9e
TD
3600 number = number.replace('.', WCF.Language.get('wcf.global.decimalPoint'));
3601
7e969bc7
TD
3602 number = this.addThousandsSeparator(number);
3603 number = number.replace('-', '\u2212');
3604
3605 return number;
158bd3ca
TD
3606 },
3607
7a17b105
MS
3608 /**
3609 * Makes a string's first character lowercase
3610 *
3611 * @param string string
3612 * @return string
3613 */
3614 lcfirst: function(string) {
bf3df436 3615 return String(string).substring(0, 1).toLowerCase() + string.substring(1);
7a17b105
MS
3616 },
3617
158bd3ca 3618 /**
52560d9e 3619 * Makes a string's first character uppercase
158bd3ca 3620 *
52560d9e 3621 * @param string string
158bd3ca
TD
3622 * @return string
3623 */
52560d9e 3624 ucfirst: function(string) {
bf3df436 3625 return String(string).substring(0, 1).toUpperCase() + string.substring(1);
cc7db7fa
AE
3626 },
3627
3628 /**
3629 * Unescapes special HTML-characters within a string
3630 *
3631 * @param string string
3632 * @return string
3633 */
3634 unescapeHTML: function (string) {
3635 return String(string).replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
158bd3ca
TD
3636 }
3637};
3638
3639/**
3640 * Basic implementation for WCF TabMenus. Use the data attributes 'active' to specify the
3641 * tab which should be shown on init. Furthermore you may specify a 'store' data-attribute
3642 * which will be filled with the currently selected tab.
3643 */
3644WCF.TabMenu = {
dbd319de
AE
3645 /**
3646 * list of tabmenu containers
3647 * @var object
3648 */
3649 _containers: { },
3650
f24f0823
AE
3651 /**
3652 * initialization state
3653 * @var boolean
3654 */
dbd319de 3655 _didInit: false,
f24f0823 3656
158bd3ca
TD
3657 /**
3658 * Initializes all TabMenus
3659 */
3660 init: function() {
0190e4b6 3661 var $containers = $('.tabMenuContainer:not(.staticTabMenuContainer)');
dbd319de 3662 var self = this;
f24f0823
AE
3663 $containers.each(function(index, tabMenu) {
3664 var $tabMenu = $(tabMenu);
dbd319de
AE
3665 var $containerID = $tabMenu.wcfIdentify();
3666 if (self._containers[$containerID]) {
3667 // continue with next container
3668 return true;
158bd3ca
TD
3669 }
3670
016198a8
AE
3671 if ($tabMenu.data('store') && !$('#' + $tabMenu.data('store')).length) {
3672 $('<input type="hidden" name="' + $tabMenu.data('store') + '" value="" id="' + $tabMenu.data('store') + '" />').appendTo($tabMenu.parents('form').find('.formSubmit'));
3673 }
3674
158bd3ca 3675 // init jQuery UI TabMenu
dbd319de 3676 self._containers[$containerID] = $tabMenu;
f24f0823 3677 $tabMenu.wcfTabs({
22b63e33 3678 active: false,
01ec877d
AE
3679 activate: function(event, eventData) {
3680 var $panel = $(eventData.newPanel);
184a8d6d 3681 var $container = $panel.closest('.tabMenuContainer');
158bd3ca
TD
3682
3683 // store currently selected item
016198a8
AE
3684 var $tabMenu = $container;
3685 while (true) {
3686 // do not trigger on init
3687 if ($tabMenu.data('isParent') === undefined) {
3688 break;
3689 }
3690
3691 if ($tabMenu.data('isParent')) {
3692 if ($tabMenu.data('store')) {
3693 $('#' + $tabMenu.data('store')).val($panel.attr('id'));
3694 }
3695
3696 break;
3697 }
3698 else {
3699 $tabMenu = $tabMenu.data('parent');
158bd3ca
TD
3700 }
3701 }
f24f0823
AE
3702
3703 // set panel id as location hash
dbd319de 3704 if (WCF.TabMenu._didInit) {
9ad87a1c
AE
3705 // do not update history if within an overlay
3706 if ($panel.data('inTabMenu') == undefined) {
3707 $panel.data('inTabMenu', ($panel.parents('.dialogContainer').length));
535be52b 3708 }
9ad87a1c
AE
3709
3710 if (!$panel.data('inTabMenu')) {
3711 if (window.history) {
3712 window.history.pushState(null, document.title, window.location.toString().replace(/#.+$/, '') + '#' + $panel.attr('id'));
3713 }
3714 else {
3715 location.hash = '#' + $panel.attr('id');
3716 }
535be52b 3717 }
f24f0823 3718 }
158bd3ca
TD
3719 }
3720 });
3721
47e7e4d9 3722 $tabMenu.data('isParent', ($tabMenu.children('.tabMenuContainer, .tabMenuContent').length > 0)).data('parent', false);
4b1f0bb6
AE
3723 if (!$tabMenu.data('isParent')) {
3724 // check if we're a child element
3725 if ($tabMenu.parent().hasClass('tabMenuContainer')) {
3726 $tabMenu.data('parent', $tabMenu.parent());
3727 }
3728 }
158bd3ca 3729 });
f24f0823
AE
3730
3731 // try to resolve location hash
dbd319de 3732 if (!this._didInit) {
01ec877d 3733 this._selectActiveTab();
dbd319de 3734 $(window).bind('hashchange', $.proxy(this.selectTabs, this));
da804832 3735
afa99e58 3736 if (!this._selectErroneousTab()) {
01ec877d 3737 this.selectTabs();
da804832 3738 }
dbd319de
AE
3739 }
3740
da804832
AE
3741 this._didInit = true;
3742 },
3743
1eda8a97
MS
3744 /**
3745 * Reloads the tab menus.
3746 */
3747 reload: function() {
3748 this._containers = { };
3749 this.init();
3750 },
3751
da804832 3752 /**
28410a97
MS
3753 * Force display of first erroneous tab and returns true if at least one
3754 * tab contains an error.
da804832
AE
3755 *
3756 * @return boolean
3757 */
3758 _selectErroneousTab: function() {
4b1f0bb6
AE
3759 for (var $containerID in this._containers) {
3760 var $tabMenu = this._containers[$containerID];
da804832 3761
4b1f0bb6
AE
3762 if (!$tabMenu.data('isParent') && $tabMenu.find('.formError').length) {
3763 while (true) {
3764 if ($tabMenu.data('parent') === false) {
3765 break;
3766 }
3767
22b63e33 3768 $tabMenu = $tabMenu.data('parent').wcfTabs('selectTab', $tabMenu.wcfIdentify());
4b1f0bb6
AE
3769 }
3770
da804832 3771 return true;
4b1f0bb6
AE
3772 }
3773 }
3774
da804832
AE
3775 return false;
3776 },
3777
3778 /**
3779 * Selects the active tab menu item.
3780 */
3781 _selectActiveTab: function() {
3782 for (var $containerID in this._containers) {
3783 var $tabMenu = this._containers[$containerID];
3784 if ($tabMenu.data('active')) {
3785 var $index = $tabMenu.data('active');
3786 var $subIndex = null;
3787 if (/-/.test($index)) {
3788 var $tmp = $index.split('-');
3789 $index = $tmp[0];
3790 $subIndex = $tmp[1];
3791 }
3792
3793 $tabMenu.find('.tabMenuContent').each(function(innerIndex, tabMenuItem) {
3794 var $tabMenuItem = $(tabMenuItem);
3795 if ($tabMenuItem.wcfIdentify() == $index) {
3796 $tabMenu.wcfTabs('select', innerIndex);
da804832
AE
3797 if ($subIndex !== null) {
3798 if ($tabMenuItem.hasClass('tabMenuContainer')) {
22b63e33 3799 $tabMenuItem.wcfTabs('selectTab', $tabMenu.data('active'));
da804832
AE
3800 }
3801 else {
22b63e33 3802 $tabMenu.wcfTabs('selectTab', $tabMenu.data('active'));
da804832
AE
3803 }
3804 }
3805
3806 return false;
3807 }
3808 });
3809 }
3810 }
dbd319de
AE
3811 },
3812
3813 /**
3814 * Resolves location hash to display tab menus.
d0fac0bd
AE
3815 *
3816 * @return boolean
dbd319de
AE
3817 */
3818 selectTabs: function() {
3819 if (location.hash) {
3820 var $hash = location.hash.substr(1);
dbd319de 3821
d0fac0bd
AE
3822 // try to find matching tab menu container
3823 var $tabMenu = $('#' + $.wcfEscapeID($hash));
3824 if ($tabMenu.length === 1 && $tabMenu.hasClass('ui-tabs-panel')) {
3825 $tabMenu = $tabMenu.parent('.ui-tabs');
3826 if ($tabMenu.length) {
22b63e33 3827 $tabMenu.wcfTabs('selectTab', $hash);
d0fac0bd
AE
3828
3829 // check if this is a nested tab menu
3830 if ($tabMenu.hasClass('ui-tabs-panel')) {
3831 $hash = $tabMenu.wcfIdentify();
3832 $tabMenu = $tabMenu.parent('.ui-tabs');
3833 if ($tabMenu.length) {
22b63e33 3834 $tabMenu.wcfTabs('selectTab', $hash);
f24f0823 3835 }
f24f0823 3836 }
dbd319de 3837
d0fac0bd 3838 return true;
dbd319de 3839 }
f24f0823
AE
3840 }
3841 }
d0fac0bd
AE
3842
3843 return false;
158bd3ca
TD
3844 }
3845};
3846
3847/**
9f959ced
MS
3848 * Templates that may be fetched more than once with different variables.
3849 * Based upon ideas from Prototype's template.
158bd3ca
TD
3850 *
3851 * Usage:
3852 * var myTemplate = new WCF.Template('{$hello} World');
3853 * myTemplate.fetch({ hello: 'Hi' }); // Hi World
3854 * myTemplate.fetch({ hello: 'Hello' }); // Hello World
3855 *
3856 * my2ndTemplate = new WCF.Template('{@$html}{$html}');
3857 * my2ndTemplate.fetch({ html: '<b>Test</b>' }); // <b>Test</b>&lt;b&gt;Test&lt;/b&gt;
9f959ced 3858 *
158bd3ca
TD
3859 * var my3rdTemplate = new WCF.Template('You can use {literal}{$variable}{/literal}-Tags here');
3860 * my3rdTemplate.fetch({ variable: 'Not shown' }); // You can use {$variable}-Tags here
3861 *
158bd3ca
TD
3862 * @param template template-content
3863 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/template.js
3864 */
39e27190 3865WCF.Template = Class.extend({
158bd3ca
TD
3866 /**
3867 * Prepares template
3868 *
3869 * @param $template template-content
3870 */
955082b0 3871 init: function(template) {
35a7384e 3872 var $literals = new WCF.Dictionary();
66a11454 3873 var $tagID = 0;
35a7384e 3874
ec591c4f
TD
3875 // escape \ and ' and newlines
3876 template = template.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/(\r\n|\n|\r)/g, '\\n');
158bd3ca
TD
3877
3878 // save literal-tags
ec591c4f 3879 template = template.replace(/\{literal\}(.*?)\{\/literal\}/g, $.proxy(function(match) {
158bd3ca
TD
3880 // hopefully no one uses this string in one of his templates
3881 var id = '@@@@@@@@@@@'+Math.random()+'@@@@@@@@@@@';
35a7384e 3882 $literals.add(id, match.replace(/\{\/?literal\}/g, ''));
158bd3ca
TD
3883
3884 return id;
3885 }, this));
158bd3ca 3886
16872914
TD
3887 // remove comments
3888 template = template.replace(/\{\*.*?\*\}/g, '');
3889
ec591c4f 3890 var parseParameterList = function(parameterString) {
6c9ce157
TD
3891 var $chars = parameterString.split('');
3892 var $parameters = { };
3893 var $inName = true;
3894 var $name = '';
3895 var $value = '';
a446eaa4
TD
3896 var $doubleQuoted = false;
3897 var $singleQuoted = false;
6c9ce157 3898 var $escaped = false;
ec591c4f 3899
6c9ce157
TD
3900 for (var $i = 0, $max = $chars.length; $i < $max; $i++) {
3901 var $char = $chars[$i];
89be55b6 3902 if ($inName && $char != '=' && $char != ' ') $name += $char;
6c9ce157
TD
3903 else if ($inName && $char == '=') {
3904 $inName = false;
a446eaa4
TD
3905 $singleQuoted = false;
3906 $doubleQuoted = false;
6c9ce157
TD
3907 $escaped = false;
3908 }
a446eaa4 3909 else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == ' ') {
6c9ce157
TD
3910 $inName = true;
3911 $parameters[$name] = $value;
3912 $value = $name = '';
3913 }
a446eaa4
TD
3914 else if (!$inName && $singleQuoted && !$escaped && $char == "'") {
3915 $singleQuoted = false;
6c9ce157
TD
3916 $value += $char;
3917 }
a446eaa4
TD
3918 else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == "'") {
3919 $singleQuoted = true;
6c9ce157
TD
3920 $value += $char;
3921 }
a446eaa4
TD
3922 else if (!$inName && $doubleQuoted && !$escaped && $char == '"') {
3923 $doubleQuoted = false;
3924 $value += $char;
3925 }
3926 else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == '"') {
3927 $doubleQuoted = true;
3928 $value += $char;
3929 }
3930 else if (!$inName && ($doubleQuoted || $singleQuoted) && !$escaped && $char == '\\') {
6c9ce157
TD
3931 $escaped = true;
3932 $value += $char;
3933 }
3934 else if (!$inName) {
3935 $escaped = false;
3936 $value += $char;
3937 }
3938 }
3939 $parameters[$name] = $value;
3940
a446eaa4 3941 if ($doubleQuoted || $singleQuoted || $escaped) throw new Error('Syntax error in parameterList: "' + parameterString + '"');
6c9ce157
TD
3942
3943 return $parameters;
3944 };
3945
ec591c4f
TD
3946 var unescape = function(string) {
3947 return string.replace(/\\n/g, "\n").replace(/\\\\/g, '\\').replace(/\\'/g, "'");
3948 };
3949
4a107b66 3950 template = template.replace(/\{(\$[^\}]+?)\}/g, function(_, content) {
5a6819fe 3951 content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])"));
955082b0 3952
35a7384e 3953 return "' + WCF.String.escapeHTML(" + content + ") + '";
ec591c4f
TD
3954 })
3955 // Numeric Variable
4a107b66 3956 .replace(/\{#(\$[^\}]+?)\}/g, function(_, content) {
5a6819fe 3957 content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])"));
955082b0 3958
35a7384e 3959 return "' + WCF.String.formatNumeric(" + content + ") + '";
ec591c4f
TD
3960 })
3961 // Variable without escaping
4a107b66 3962 .replace(/\{@(\$[^\}]+?)\}/g, function(_, content) {
5a6819fe 3963 content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])"));
955082b0 3964
35a7384e 3965 return "' + " + content + " + '";
35a7384e 3966 })
4650d4be 3967 // {lang}foo{/lang}
7b608580 3968 .replace(/{lang}(.+?){\/lang}/g, function(_, content) {
4650d4be
MS
3969 return "' + WCF.Language.get('" + unescape(content) + "') + '";
3970 })
ec591c4f
TD
3971 // {if}
3972 .replace(/\{if (.+?)\}/g, function(_, content) {
5a6819fe 3973 content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])"));
0f5aa615 3974
ec591c4f
TD
3975 return "';\n" +
3976 "if (" + content + ") {\n" +
3977 " $output += '";
35a7384e 3978 })
ec591c4f
TD
3979 // {elseif}
3980 .replace(/\{else ?if (.+?)\}/g, function(_, content) {
5a6819fe 3981 content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])"));
ec591c4f
TD
3982
3983 return "';\n" +
3984 "}\n" +
3985 "else if (" + content + ") {\n" +
3986 " $output += '";
3987 })
3988 // {implode}
3989 .replace(/\{implode (.+?)\}/g, function(_, content) {
66a11454 3990 $tagID++;
6c9ce157
TD
3991
3992 content = content.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
3993 var $parameters = parseParameterList(content);
3994
3995 if (typeof $parameters['from'] === 'undefined') throw new Error('Missing from attribute in implode-tag');
3996 if (typeof $parameters['item'] === 'undefined') throw new Error('Missing item attribute in implode-tag');
3997 if (typeof $parameters['glue'] === 'undefined') $parameters['glue'] = "', '";
ec591c4f 3998
5a6819fe 3999 $parameters['from'] = $parameters['from'].replace(/\$([^.\[\s]+)/g, "(v.$1)");
6c9ce157 4000
ec591c4f
TD
4001 return "';\n"+
4002 "var $implode_" + $tagID + " = false;\n" +
4003 "for ($implodeKey_" + $tagID + " in " + $parameters['from'] + ") {\n" +
4004 " v[" + $parameters['item'] + "] = " + $parameters['from'] + "[$implodeKey_" + $tagID + "];\n" +
4005 (typeof $parameters['key'] !== 'undefined' ? " v[" + $parameters['key'] + "] = $implodeKey_" + $tagID + ";\n" : "") +
4006 " if ($implode_" + $tagID + ") $output += " + $parameters['glue'] + ";\n" +
4007 " $implode_" + $tagID + " = true;\n" +
4008 " $output += '";
66a11454 4009 })
ec591c4f
TD
4010 // {foreach}
4011 .replace(/\{foreach (.+?)\}/g, function(_, content) {
66a11454
TD
4012 $tagID++;
4013
4014 content = content.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
4015 var $parameters = parseParameterList(content);
4016
4017 if (typeof $parameters['from'] === 'undefined') throw new Error('Missing from attribute in foreach-tag');
4018 if (typeof $parameters['item'] === 'undefined') throw new Error('Missing item attribute in foreach-tag');
5a6819fe 4019 $parameters['from'] = $parameters['from'].replace(/\$([^.\[\s]+)/g, "(v.$1)");
66a11454 4020
ec591c4f
TD
4021 return "';\n" +
4022 "$foreach_"+$tagID+" = false;\n" +
4023 "for ($foreachKey_" + $tagID + " in " + $parameters['from'] + ") {\n" +
4024 " $foreach_"+$tagID+" = true;\n" +
4025 " break;\n" +
4026 "}\n" +
4027 "if ($foreach_"+$tagID+") {\n" +
4028 " for ($foreachKey_" + $tagID + " in " + $parameters['from'] + ") {\n" +
4029 " v[" + $parameters['item'] + "] = " + $parameters['from'] + "[$foreachKey_" + $tagID + "];\n" +
4030 (typeof $parameters['key'] !== 'undefined' ? " v[" + $parameters['key'] + "] = $foreachKey_" + $tagID + ";\n" : "") +
4031 " $output += '";
6c9ce157 4032 })
ec591c4f
TD
4033 // {foreachelse}
4034 .replace(/\{foreachelse\}/g,
4035 "';\n" +
4036 " }\n" +
4037 "}\n" +
4038 "else {\n" +
4039 " {\n" +
4040 " $output += '"
4041 )
4042 // {/foreach}
4043 .replace(/\{\/foreach\}/g,
4044 "';\n" +
4045 " }\n" +
4046 "}\n" +
4047 "$output += '"
4048 )
4049 // {else}
4050 .replace(/\{else\}/g,
4051 "';\n" +
4052 "}\n" +
4053 "else {\n" +
4054 " $output += '"
4055 )
4056 // {/if} and {/implode}
4057 .replace(/\{\/(if|implode)\}/g,
4058 "';\n" +
4059 "}\n" +
4060 "$output += '"
4061 );
4062
4063 // call callback
71b79d8a
TD
4064 for (var key in WCF.Template.callbacks) {
4065 template = WCF.Template.callbacks[key](template);
158bd3ca
TD
4066 }
4067
4068 // insert delimiter tags
35a7384e 4069 template = template.replace('{ldelim}', '{').replace('{rdelim}', '}');
158bd3ca 4070
ec591c4f 4071 $literals.each(function(pair) {
35a7384e 4072 template = template.replace(pair.key, pair.value);
158bd3ca
TD
4073 });
4074
35a7384e 4075 template = "$output += '" + template + "';";
4a107b66 4076
a2067610 4077 try {
e338ca2c 4078 this.fetch = new Function("v", "if (typeof v != 'object') { v = {}; } v.__window = window; v.__wcf = window.WCF; var $output = ''; " + template + ' return $output;');
a2067610
TD
4079 }
4080 catch (e) {
4081 console.debug("var $output = ''; " + template + ' return $output;');
4082 throw e;
4083 }
158bd3ca
TD
4084 },
4085
4086 /**
35a7384e 4087 * Fetches the template with the given variables.
7b608580 4088 *
955082b0
TD
4089 * @param v variables to insert
4090 * @return parsed template
158bd3ca 4091 */
520d73f1 4092 fetch: function(v) {
35a7384e 4093 // this will be replaced in the init function
158bd3ca 4094 }
39e27190 4095});
158bd3ca
TD
4096
4097/**
71b79d8a 4098 * Array of callbacks that will be called after parsing the included tags. Only applies to Templates compiled after the callback was added.
158bd3ca 4099 *
71b79d8a 4100 * @var array<Function>
158bd3ca 4101 */
71b79d8a 4102WCF.Template.callbacks = [ ];
158bd3ca
TD
4103
4104/**
4105 * Toggles options.
4106 *
4107 * @param string element
4108 * @param array showItems
4109 * @param array hideItems
14c5ff9e 4110 * @param function callback
158bd3ca 4111 */
39e27190 4112WCF.ToggleOptions = Class.extend({
158bd3ca
TD
4113 /**
4114 * target item
4115 *
4116 * @var jQuery
4117 */
4118 _element: null,
4119
4120 /**
4121 * list of items to be shown
4122 *
4123 * @var array
4124 */
4125 _showItems: [],
4126
4127 /**
4128 * list of items to be hidden
4129 *
4130 * @var array
4131 */
4132 _hideItems: [],
14c5ff9e
DR
4133
4134 /**
4135 * callback after options were toggled
4136 *
4137 * @var function
4138 */
4139 _callback: null,
158bd3ca
TD
4140
4141 /**
4142 * Initializes option toggle.
4143 *
4144 * @param string element
4145 * @param array showItems
4146 * @param array hideItems
14c5ff9e 4147 * @param function callback
158bd3ca 4148 */
14c5ff9e 4149 init: function(element, showItems, hideItems, callback) {
158bd3ca
TD
4150 this._element = $('#' + element);
4151 this._showItems = showItems;
4152 this._hideItems = hideItems;
14c5ff9e
DR
4153 if (callback !== undefined) {
4154 this._callback = callback;
4155 }
158bd3ca
TD
4156
4157 // bind event
4158 this._element.click($.proxy(this._toggle, this));
4159
4160 // execute toggle on init
4161 this._toggle();
4162 },
4163
4164 /**
4165 * Toggles items.
4166 */
4167 _toggle: function() {
1a50aae0 4168 if (!this._element.prop('checked')) return;
158bd3ca
TD
4169
4170 for (var $i = 0, $length = this._showItems.length; $i < $length; $i++) {
4171 var $item = this._showItems[$i];
4172
4173 $('#' + $item).show();
4174 }
4175
4176 for (var $i = 0, $length = this._hideItems.length; $i < $length; $i++) {
4177 var $item = this._hideItems[$i];
4178
4179 $('#' + $item).hide();
4180 }
14c5ff9e
DR
4181
4182 if (this._callback !== null) {
e42f6447 4183 this._callback();
14c5ff9e 4184 }
158bd3ca 4185 }
39e27190 4186});
158bd3ca 4187
1c746fe3
AE
4188/**
4189 * Namespace for all kind of collapsible containers.
4190 */
4191WCF.Collapsible = {};
4192
4193/**
4194 * Simple implementation for collapsible content, neither does it
4195 * store its state nor does it allow AJAX callbacks to fetch content.
4196 */
4197WCF.Collapsible.Simple = {
4198 /**
4199 * Initializes collapsibles.
4200 */
4201 init: function() {
b29fbb53 4202 $('.jsCollapsible').each($.proxy(function(index, button) {
1c746fe3
AE
4203 this._initButton(button);
4204 }, this));
4205 },
4206
4207 /**
4208 * Binds an event listener on all buttons triggering the collapsible.
4209 *
4210 * @param object button
4211 */
4212 _initButton: function(button) {
4213 var $button = $(button);
4214 var $isOpen = $button.data('isOpen');
4215
4216 if (!$isOpen) {
4217 // hide container on init
ec161ab2 4218 $('#' + $button.data('collapsibleContainer')).hide();
1c746fe3
AE
4219 }
4220
4221 $button.click($.proxy(this._toggle, this));
4222 },
4223
4224 /**
4225 * Toggles collapsible containers on click.
4226 *
4227 * @param object event
4228 */
4229 _toggle: function(event) {
ec161ab2 4230 var $button = $(event.currentTarget);
1c746fe3
AE
4231 var $isOpen = $button.data('isOpen');
4232 var $target = $('#' + $.wcfEscapeID($button.data('collapsibleContainer')));
4233
4234 if ($isOpen) {
4235 $target.stop().wcfBlindOut('vertical', $.proxy(function() {
556973c1 4236 this._toggleImage($button);
1c746fe3
AE
4237 }, this));
4238 $isOpen = false;
4239 }
4240 else {
4241 $target.stop().wcfBlindIn('vertical', $.proxy(function() {
556973c1 4242 this._toggleImage($button);
1c746fe3
AE
4243 }, this));
4244 $isOpen = true;
4245 }
4246
4247 $button.data('isOpen', $isOpen);
4248
4249 // suppress event
4250 event.stopPropagation();
4251 return false;
4252 },
4253
4254 /**
4255 * Toggles image of target button.
4256 *
4257 * @param jQuery button
1c746fe3 4258 */
556973c1
MW
4259 _toggleImage: function(button) {
4260 var $icon = button.find('span.icon');
4261 if (button.data('isOpen')) {
4262 $icon.removeClass('icon-chevron-right').addClass('icon-chevron-down');
4263 }
4264 else {
4265 $icon.removeClass('icon-chevron-down').addClass('icon-chevron-right');
1c746fe3 4266 }
1c746fe3
AE
4267 }
4268};
4269
878d0d80
AE
4270/**
4271 * Basic implementation for collapsible containers with AJAX support. Results for open
4272 * and closed state will be cached.
9f959ced 4273 *
878d0d80
AE
4274 * @param string className
4275 */
4276WCF.Collapsible.Remote = Class.extend({
4277 /**
4278 * class name
4279 * @var string
4280 */
4281 _className: '',
9f959ced 4282
878d0d80
AE
4283 /**
4284 * list of active containers
4285 * @var object
4286 */
4287 _containers: {},
9f959ced 4288
878d0d80
AE
4289 /**
4290 * container meta data
4291 * @var object
4292 */
4293 _containerData: {},
9f959ced 4294
878d0d80
AE
4295 /**
4296 * action proxy
4297 * @var WCF.Action.Proxy
4298 */
4299 _proxy: null,
9f959ced 4300
878d0d80
AE
4301 /**
4302 * Initializes the controller for collapsible containers with AJAX support.
4303 *
4304 * @param string className
4305 */
4306 init: function(className) {
4307 this._className = className;
878d0d80
AE
4308 this._proxy = new WCF.Action.Proxy({
4309 success: $.proxy(this._success, this)
4310 });
4311
4312 // initialize each container
0959ca1d
AE
4313 this._init();
4314
4315 WCF.DOMNodeInsertedHandler.addCallback('WCF.Collapsible.Remote', $.proxy(this._init, this));
4316 },
4317
4318 /**
4319 * Initializes a collapsible container.
4320 *
4321 * @param string containerID
4322 */
4323 _init: function(containerID) {
4324 this._getContainers().each($.proxy(function(index, container) {
878d0d80 4325 var $container = $(container);
c4b3ae32 4326 var $containerID = $container.wcfIdentify();
878d0d80 4327
0959ca1d
AE
4328 if (this._containers[$containerID] === undefined) {
4329 this._containers[$containerID] = $container;
4330
4331 this._initContainer($containerID);
4332 }
878d0d80
AE
4333 }, this));
4334 },
4335
c4b3ae32
AE
4336 /**
4337 * Initializes a collapsible container.
4338 *
4339 * @param string containerID
4340 */
8805f7bf 4341 _initContainer: function(containerID) {
77c43423
MW
4342 var $target = this._getTarget(containerID);
4343 var $buttonContainer = this._getButtonContainer(containerID);
4344 var $button = this._createButton(containerID, $buttonContainer);
4345
4346 // store container meta data
4347 this._containerData[containerID] = {
4348 button: $button,
4349 buttonContainer: $buttonContainer,
c4b3ae32 4350 isOpen: this._containers[containerID].data('isOpen'),
77c43423
MW
4351 target: $target
4352 };
2851eadd
MS
4353
4354 // add 'jsCollapsed' CSS class
4355 if (!this._containers[containerID].data('isOpen')) {
4356 $('#' + containerID).addClass('jsCollapsed');
4357 }
77c43423
MW
4358 },
4359
878d0d80
AE
4360 /**
4361 * Returns a collection of collapsible containers.
4362 *
4363 * @return jQuery
4364 */
4365 _getContainers: function() { },
4366
4367 /**
4368 * Returns the target element for current collapsible container.
4369 *
4370 * @param integer containerID
4371 * @return jQuery
4372 */
4373 _getTarget: function(containerID) { },
4374
4375 /**
4376 * Returns the button container for current collapsible container.
4377 *
4378 * @param integer containerID
4379 * @return jQuery
4380 */
4381 _getButtonContainer: function(containerID) { },
4382
4383 /**
4384 * Creates the toggle button.
4385 *
4386 * @param integer containerID
4387 * @param jQuery buttonContainer
4388 */
4389 _createButton: function(containerID, buttonContainer) {
4390 var $isOpen = this._containers[containerID].data('isOpen');
556973c1 4391 var $button = $('<span class="collapsibleButton jsTooltip pointer icon icon16 icon-' + ($isOpen ? 'chevron-down' : 'chevron-right') + '" title="'+WCF.Language.get('wcf.global.button.collapsible')+'">').prependTo(buttonContainer);
878d0d80 4392 $button.data('containerID', containerID).click($.proxy(this._toggleContainer, this));
9f959ced 4393
03812bbc 4394 return $button;
878d0d80
AE
4395 },
4396
4397 /**
4398 * Toggles a container.
4399 *
4400 * @param object event
4401 */
4402 _toggleContainer: function(event) {
b8a3ccb7 4403 var $button = $(event.currentTarget);
878d0d80 4404 var $containerID = $button.data('containerID');
f4126129 4405 var $isOpen = this._containerData[$containerID].isOpen;
878d0d80
AE
4406 var $state = ($isOpen) ? 'open' : 'close';
4407 var $newState = ($isOpen) ? 'close' : 'open';
4408
878d0d80
AE
4409 // fetch content state via AJAX
4410 this._proxy.setOption('data', {
c4b3ae32 4411 actionName: 'loadContainer',
878d0d80 4412 className: this._className,
c05528d8 4413 interfaceName: 'wcf\\data\\ILoadableContainerAction',
c4b3ae32 4414 objectIDs: [ this._getObjectID($containerID) ],
88a85f47 4415 parameters: $.extend(true, {
878d0d80
AE
4416 containerID: $containerID,
4417 currentState: $state,
a083bb38 4418 newState: $newState
88a85f47 4419 }, this._getAdditionalParameters($containerID))
878d0d80
AE
4420 });
4421 this._proxy.sendRequest();
9f959ced 4422
2851eadd
MS
4423 // toogle 'jsCollapsed' CSS class
4424 $('#' + $containerID).toggleClass('jsCollapsed');
4425
03812bbc 4426 // set spinner for current button
556973c1 4427 // this._exchangeIcon($button);
03812bbc 4428 },
c4b3ae32
AE
4429
4430 /**
4431 * Exchanges button icon.
4432 *
4433 * @param jQuery button
4434 * @param string newIcon
4435 */
4436 _exchangeIcon: function(button, newIcon) {
556973c1
MW
4437 newIcon = newIcon || 'spinner';
4438 button.removeClass('icon-chevron-down icon-chevron-right icon-spinner').addClass('icon-' + newIcon);
878d0d80
AE
4439 },
4440
4441 /**
4442 * Returns the object id for current container.
4443 *
4444 * @param integer containerID
4445 * @return integer
4446 */
eac3b734
MS
4447 _getObjectID: function(containerID) {
4448 return $('#' + containerID).data('objectID');
4449 },
878d0d80 4450
88a85f47
MW
4451 /**
4452 * Returns additional parameters.
4453 *
4454 * @param integer containerID
4455 * @return object
4456 */
4457 _getAdditionalParameters: function(containerID) {
4458 return {};
4459 },
4460
77c43423
MW
4461 /**
4462 * Updates container content.
4463 *
4464 * @param integer containerID
4465 * @param string newContent
4466 * @param string newState
4467 */
4468 _updateContent: function(containerID, newContent, newState) {
4469 this._containerData[containerID].target.html(newContent);
4470 },
4471
878d0d80
AE
4472 /**
4473 * Sets content upon successfull AJAX request.
4474 *
4475 * @param object data
4476 * @param string textStatus
4477 * @param jQuery jqXHR
4478 */
4479 _success: function(data, textStatus, jqXHR) {
4480 // validate container id
4481 if (!data.returnValues.containerID) return;
4482 var $containerID = data.returnValues.containerID;
4483
4484 // check if container id is known
4485 if (!this._containers[$containerID]) return;
4486
4487 // update content storage
4488 this._containerData[$containerID].isOpen = (data.returnValues.isOpen) ? true : false;
9fd51bd2 4489 var $newState = (data.returnValues.isOpen) ? 'open' : 'close';
878d0d80
AE
4490
4491 // update container content
b418da85 4492 this._updateContent($containerID, $.trim(data.returnValues.content), $newState);
b8a3ccb7
MW
4493
4494 // update icon
556973c1 4495 this._exchangeIcon(this._containerData[$containerID].button, (data.returnValues.isOpen ? 'chevron-down' : 'chevron-right'));
c4b3ae32
AE
4496 }
4497});
4498
4499/**
4500 * Basic implementation for collapsible containers with AJAX support. Requires collapsible
4501 * content to be available in DOM already, if you want to load content on the fly use
4502 * WCF.Collapsible.Remote instead.
4503 */
4504WCF.Collapsible.SimpleRemote = WCF.Collapsible.Remote.extend({
4505 /**
4506 * Initializes an AJAX-based collapsible handler.
4507 *
4508 * @param string className
4509 */
4510 init: function(className) {
4511 this._super(className);
4512
4513 // override settings for action proxy
4514 this._proxy = new WCF.Action.Proxy({
4515 showLoadingOverlay: false
4516 });
4517 },
4518
f19a9976
AE
4519 /**
4520 * @see WCF.Collapsible.Remote._initContainer()
4521 */
4522 _initContainer: function(containerID) {
4523 this._super(containerID);
4524
4525 // hide container on init if applicable
4526 if (!this._containerData[containerID].isOpen) {
4527 this._containerData[containerID].target.hide();
143c5f6d 4528 this._exchangeIcon(this._containerData[containerID].button, 'chevron-right');
f19a9976
AE
4529 }
4530 },
4531
c4b3ae32
AE
4532 /**
4533 * Toggles container visibility.
4534 *
4535 * @param object event
4536 */
4537 _toggleContainer: function(event) {
4538 var $button = $(event.currentTarget);
4539 var $containerID = $button.data('containerID');
4540 var $isOpen = this._containerData[$containerID].isOpen;
4541 var $currentState = ($isOpen) ? 'open' : 'close';
4542 var $newState = ($isOpen) ? 'close' : 'open';
4543
4544 this._proxy.setOption('data', {
4545 actionName: 'toggleContainer',
4546 className: this._className,
c05528d8 4547 interfaceName: 'wcf\\data\\IToggleContainerAction',
c4b3ae32 4548 objectIDs: [ this._getObjectID($containerID) ],
11c6026b 4549 parameters: $.extend(true, {
c4b3ae32
AE
4550 containerID: $containerID,
4551 currentState: $currentState,
4552 newState: $newState
11c6026b 4553 }, this._getAdditionalParameters($containerID))
c4b3ae32
AE
4554 });
4555 this._proxy.sendRequest();
4556
4557 // exchange icon
143c5f6d 4558 this._exchangeIcon(this._containerData[$containerID].button, ($newState === 'open' ? 'chevron-down' : 'chevron-right'));
6ea6d38b
AE
4559
4560 // toggle container
4561 if ($newState === 'open') {
4562 this._containerData[$containerID].target.show();
4563 }
4564 else {
4565 this._containerData[$containerID].target.hide();
4566 }
4567
2851eadd
MS
4568 // toogle 'jsCollapsed' CSS class
4569 $('#' + $containerID).toggleClass('jsCollapsed');
4570
6ea6d38b
AE
4571 // update container data
4572 this._containerData[$containerID].isOpen = ($newState === 'open' ? true : false);
878d0d80
AE
4573 }
4574});
9abd2c89 4575
168a9753
AE
4576/**
4577 * Provides collapsible sidebars with persistency support.
4578 */
9abd2c89 4579WCF.Collapsible.Sidebar = Class.extend({
168a9753
AE
4580 /**
4581 * trigger button object
4582 * @var jQuery
4583 */
9abd2c89 4584 _button: null,
168a9753 4585
a3574790
AE
4586 /**
4587 * trigger button height
4588 * @var integer
4589 */
4590 _buttonHeight: 0,
4591
168a9753
AE
4592 /**
4593 * sidebar state
4594 * @var boolean
4595 */
9abd2c89 4596 _isOpen: false,
168a9753 4597
a3574790
AE
4598 /**
4599 * main container object
4600 * @var jQuery
4601 */
4602 _mainContainer: null,
4603
168a9753
AE
4604 /**
4605 * action proxy
4606 * @var WCF.Action.Proxy
4607 */
9abd2c89 4608 _proxy: null,
168a9753
AE
4609
4610 /**
4611 * sidebar object
4612 * @var jQuery
4613 */
9abd2c89 4614 _sidebar: null,
168a9753 4615
a3574790
AE
4616 /**
4617 * sidebar height
4618 * @var integer
4619 */
4620 _sidebarHeight: 0,
4621
168a9753
AE
4622 /**
4623 * sidebar identifier
4624 * @var string
4625 */
9abd2c89
AE
4626 _sidebarName: '',
4627
a3574790
AE
4628 /**
4629 * sidebar offset from document top
4630 * @var integer
4631 */
4632 _sidebarOffset: 0,
4633
4634 /**
4635 * user panel height
4636 * @var integer
4637 */
4638 _userPanelHeight: 0,
4639
168a9753
AE
4640 /**
4641 * Creates a new WCF.Collapsible.Sidebar object.
4642 */
9abd2c89
AE
4643 init: function() {
4644 this._sidebar = $('.sidebar:eq(0)');
4645 if (!this._sidebar.length) {
4646 console.debug("[WCF.Collapsible.Sidebar] Could not find sidebar, aborting.");
4647 return;
4648 }
4649
a3574790 4650 this._isOpen = (this._sidebar.data('isOpen')) ? true : false;
9abd2c89 4651 this._sidebarName = this._sidebar.data('sidebarName');
a3574790
AE
4652 this._mainContainer = $('#main');
4653 this._sidebarHeight = this._sidebar.height();
4654 this._sidebarOffset = this._sidebar.getOffsets('offset').top;
4655 this._userPanelHeight = $('#topMenu').outerHeight();
9abd2c89
AE
4656
4657 // add toggle button
4658 this._button = $('<a class="collapsibleButton jsTooltip" title="' + WCF.Language.get('wcf.global.button.collapsible') + '" />').prependTo(this._sidebar);
35c14679 4659 this._button.wrap('<span />');
9abd2c89 4660 this._button.click($.proxy(this._click, this));
a3574790 4661 this._buttonHeight = this._button.outerHeight();
42d7d2cc
AE
4662
4663 WCF.DOMNodeInsertedHandler.execute();
9abd2c89
AE
4664
4665 this._proxy = new WCF.Action.Proxy({
a3574790
AE
4666 showLoadingOverlay: false,
4667 url: 'index.php/AJAXInvoke/?t=' + SECURITY_TOKEN + SID_ARG_2ND
9abd2c89
AE
4668 });
4669
a3574790
AE
4670 $(document).scroll($.proxy(this._scroll, this)).resize($.proxy(this._scroll, this));
4671
9abd2c89 4672 this._renderSidebar();
a3574790 4673 this._scroll();
bcf8bc61
AE
4674
4675 // fake resize event once transition has completed
4676 var $window = $(window);
4677 this._sidebar.on('webkitTransitionEnd transitionend msTransitionEnd oTransitionEnd', function() { $window.trigger('resize'); });
9abd2c89
AE
4678 },
4679
168a9753
AE
4680 /**
4681 * Handles clicks on the trigger button.
4682 */
9abd2c89
AE
4683 _click: function() {
4684 this._isOpen = (this._isOpen) ? false : true;
4685
4686 this._proxy.setOption('data', {
168a9753
AE
4687 actionName: 'toggle',
4688 className: 'wcf\\system\\user\\collapsible\\content\\UserCollapsibleSidebarHandler',
9abd2c89
AE
4689 isOpen: (this._isOpen ? 1 : 0),
4690 sidebarName: this._sidebarName
4691 });
4692 this._proxy.sendRequest();
4693
4694 this._renderSidebar();
4695 },
4696
a3574790
AE
4697 /**
4698 * Aligns the toggle button upon scroll or resize.
4699 */
4700 _scroll: function() {
4701 var $window = $(window);
4702 var $scrollOffset = $window.scrollTop();
4703
4704 // calculate top and bottom coordinates of visible sidebar
4705 var $topOffset = Math.max($scrollOffset - this._sidebarOffset, 0);
4706 var $bottomOffset = Math.min(this._mainContainer.height(), ($window.height() + $scrollOffset) - this._sidebarOffset);
4707
4708 var $buttonTop = 0;
4709 if ($bottomOffset === $topOffset) {
4710 // sidebar not within visible area
4711 $buttonTop = this._sidebarOffset + this._sidebarHeight;
4712 }
4713 else {
4714 $buttonTop = $topOffset + (($bottomOffset - $topOffset) / 2);
4715
4716 // if the user panel is above the sidebar, substract it's height
4717 var $overlap = Math.max(Math.min($topOffset - this._userPanelHeight, this._userPanelHeight), 0);
4718 if ($overlap > 0) {
4719 $buttonTop += ($overlap / 2);
4720 }
4721 }
4722
4723 // ensure the button does not exceed bottom boundaries
4724 if (($bottomOffset - $topOffset - this._userPanelHeight) < this._buttonHeight) {
4725 $buttonTop = $buttonTop - this._buttonHeight;
4726 }
4727 else {
4728 // exclude half button height
4729 $buttonTop = Math.max($buttonTop - (this._buttonHeight / 2), 0);
4730 }
4731
4732 this._button.css({ top: $buttonTop + 'px' });
4733
4734 },
4735
168a9753
AE
4736 /**
4737 * Renders the sidebar state.
4738 */
9abd2c89
AE
4739 _renderSidebar: function() {
4740 if (this._isOpen) {
f2185f99 4741 $('.sidebarOrientationLeft, .sidebarOrientationRight').removeClass('sidebarCollapsed');
9abd2c89
AE
4742 }
4743 else {
f2185f99 4744 $('.sidebarOrientationLeft, .sidebarOrientationRight').addClass('sidebarCollapsed');
9abd2c89 4745 }
f70b8234
AE
4746
4747 // update button position
4748 this._scroll();
bcf8bc61
AE
4749
4750 // IE9 does not support transitions, fire resize event manually
4751 if ($.browser.msie && $.browser.version.indexOf('9') === 0) {
4752 $(window).trigger('resize');
4753 }
9abd2c89
AE
4754 }
4755});
878d0d80 4756
fb969237
TD
4757/**
4758 * Holds userdata of the current user
4759 */
4760WCF.User = {
4761 /**
a99ebd63 4762 * id of the active user
fb969237
TD
4763 * @var integer
4764 */
4765 userID: 0,
4766
4767 /**
9f959ced 4768 * name of the active user
fb969237
TD
4769 * @var string
4770 */
4771 username: '',
4772
4773 /**
4774 * Initializes userdata
4775 *
4776 * @param integer userID
4777 * @param string username
4778 */
4779 init: function(userID, username) {
4780 this.userID = userID;
4781 this.username = username;
4782 }
4783};
4784
f84920f4
MW
4785/**
4786 * Namespace for effect-related functions.
4787 */
4788WCF.Effect = {};
4789
80e49fec
AE
4790/**
4791 * Scrolls to a specific element offset, optionally handling menu height.
4792 */
4793WCF.Effect.Scroll = Class.extend({
4794 /**
4795 * Scrolls to a specific element offset.
4796 *
4797 * @param jQuery element
4798 * @param boolean excludeMenuHeight
aa86d700 4799 * @param boolean disableAnimation
80e49fec
AE
4800 * @return boolean
4801 */
aa86d700 4802 scrollTo: function(element, excludeMenuHeight, disableAnimation) {
80e49fec
AE
4803 if (!element.length) {
4804 return true;
4805 }
4806
db316c8f 4807 var $elementOffset = element.getOffsets('offset').top;
80e49fec
AE
4808 var $documentHeight = $(document).height();
4809 var $windowHeight = $(window).height();
4810
4811 // handles menu height
7b509312 4812 /*if (excludeMenuHeight) {
80e49fec 4813 $elementOffset = Math.max($elementOffset - $('#topMenu').outerHeight(), 0);
7b509312 4814 }*/
80e49fec
AE
4815
4816 if ($elementOffset > $documentHeight - $windowHeight) {
4817 $elementOffset = $documentHeight - $windowHeight;
4818 if ($elementOffset < 0) {
4819 $elementOffset = 0;
4820 }
4821 }
4822
aa86d700
AE
4823 if (disableAnimation === true) {
4824 $('html,body').scrollTop($elementOffset);
4825 }
4826 else {
4827 $('html,body').animate({ scrollTop: $elementOffset }, 400, function (x, t, b, c, d) {
4828 return -c * ( ( t = t / d - 1 ) * t * t * t - 1) + b;
4829 });
4830 }
80e49fec
AE
4831
4832 return false;
4833 }
4834});
4835
f84920f4
MW
4836/**
4837 * Creates a smooth scroll effect.
4838 */
80e49fec 4839WCF.Effect.SmoothScroll = WCF.Effect.Scroll.extend({
f84920f4
MW
4840 /**
4841 * Initializes effect.
4842 */
4843 init: function() {
80e49fec 4844 var self = this;
e0b74ad2 4845 $(document).on('click', 'a[href$=#top],a[href$=#bottom]', function() {
f84920f4 4846 var $target = $(this.hash);
80e49fec 4847 self.scrollTo($target, true);
1b9cadb9
AE
4848
4849 return false;
f84920f4
MW
4850 });
4851 }
80e49fec 4852});
f84920f4 4853
c39544e5 4854/**
8f3284a3 4855 * Creates the balloon tool-tip.
c39544e5 4856 */
39e27190 4857WCF.Effect.BalloonTooltip = Class.extend({
e68f4d0d
AE
4858 /**
4859 * initialization state
4860 * @var boolean
4861 */
4862 _didInit: false,
9f959ced 4863
e68f4d0d
AE
4864 /**
4865 * tooltip element
4866 * @var jQuery
4867 */
4868 _tooltip: null,
9f959ced 4869
e68f4d0d
AE
4870 /**
4871 * cache viewport dimensions
4872 * @var object
4873 */
4874 _viewportDimensions: { },
9f959ced 4875
e68f4d0d
AE
4876 /**
4877 * Initializes tooltips.
4878 */
c39544e5 4879 init: function() {
3f42eecb 4880 if (jQuery.browser.mobile) return;
05011566 4881
e68f4d0d
AE
4882 if (!this._didInit) {
4883 // create empty div
184a8d6d 4884 this._tooltip = $('<div id="balloonTooltip" class="balloonTooltip"><span id="balloonTooltipText"></span><span class="pointer"><span></span></span></div>').appendTo($('body')).hide();
9f959ced 4885
e68f4d0d
AE
4886 // get viewport dimensions
4887 this._updateViewportDimensions();
9f959ced 4888
e68f4d0d
AE
4889 // update viewport dimensions on resize
4890 $(window).resize($.proxy(this._updateViewportDimensions, this));
9f959ced 4891
e68f4d0d 4892 // observe DOM changes
4bf9feab 4893 WCF.DOMNodeInsertedHandler.addCallback('WCF.Effect.BalloonTooltip', $.proxy(this.init, this));
9f959ced 4894
e68f4d0d
AE
4895 this._didInit = true;
4896 }
4897
c39544e5 4898 // init elements
72065432 4899 $('.jsTooltip').each($.proxy(this._initTooltip, this));
e68f4d0d 4900 },
9f959ced 4901
e68f4d0d
AE
4902 /**
4903 * Updates cached viewport dimensions.
4904 */
4905 _updateViewportDimensions: function() {
4906 this._viewportDimensions = $(document).getDimensions();
c39544e5
MW
4907 },
4908
e68f4d0d
AE
4909 /**
4910 * Initializes a tooltip element.
4911 *
4912 * @param integer index
4913 * @param object element
4914 */
4915 _initTooltip: function(index, element) {
4916 var $element = $(element);
c39544e5 4917
72065432
L
4918 if ($element.hasClass('jsTooltip')) {
4919 $element.removeClass('jsTooltip');
e68f4d0d 4920 var $title = $element.attr('title');
9f959ced 4921
e68f4d0d
AE
4922 // ignore empty elements
4923 if ($title !== '') {
4924 $element.data('tooltip', $title);
f4126129 4925 $element.removeAttr('title');
9f959ced 4926
e68f4d0d
AE
4927 $element.hover(
4928 $.proxy(this._mouseEnterHandler, this),
4929 $.proxy(this._mouseLeaveHandler, this)
4930 );
8cfb8ca1 4931 $element.click($.proxy(this._mouseLeaveHandler, this));
e68f4d0d
AE
4932 }
4933 }
c39544e5
MW
4934 },
4935
e68f4d0d
AE
4936 /**
4937 * Shows tooltip on hover.
4938 *
4939 * @param object event
4940 */
c39544e5
MW
4941 _mouseEnterHandler: function(event) {
4942 var $element = $(event.currentTarget);
c39544e5 4943
93430cba 4944 var $title = $element.attr('title');
ea2294bc
TD
4945 if ($title && $title !== '') {
4946 $element.data('tooltip', $title);
4947 $element.removeAttr('title');
4948 }
93430cba 4949
8cfb8ca1
MW
4950 // reset tooltip position
4951 this._tooltip.css({
4952 top: "0px",
4953 left: "0px"
4954 });
4955
e81ca6a1
MW
4956 // empty tooltip, skip
4957 if (!$element.data('tooltip')) {
4958 this._tooltip.hide();
4959 return;
4960 }
4961
e68f4d0d
AE
4962 // update text
4963 this._tooltip.children('span:eq(0)').text($element.data('tooltip'));
9f959ced 4964
e68f4d0d 4965 // get arrow
ef097134 4966 var $arrow = this._tooltip.find('.pointer');
9f959ced 4967
e68f4d0d
AE
4968 // get arrow width
4969 this._tooltip.show();
4970 var $arrowWidth = $arrow.outerWidth();
4971 this._tooltip.hide();
24c42d67 4972
e68f4d0d
AE
4973 // calculate position
4974 var $elementOffsets = $element.getOffsets('offset');
4975 var $elementDimensions = $element.getDimensions('outer');
4976 var $tooltipDimensions = this._tooltip.getDimensions('outer');
4977 var $tooltipDimensionsInner = this._tooltip.getDimensions('inner');
9f959ced 4978
e68f4d0d
AE
4979 var $elementCenter = $elementOffsets.left + Math.ceil($elementDimensions.width / 2);
4980 var $tooltipHalfWidth = Math.ceil($tooltipDimensions.width / 2);
257248cd 4981
e68f4d0d 4982 // determine alignment
9f959ced 4983 var $alignment = 'center';
e68f4d0d
AE
4984 if (($elementCenter - $tooltipHalfWidth) < 5) {
4985 $alignment = 'left';
4986 }
4987 else if ((this._viewportDimensions.width - 5) < ($elementCenter + $tooltipHalfWidth)) {
4988 $alignment = 'right';
1fe125ca 4989 }
9f959ced 4990
e68f4d0d 4991 // calculate top offset
c8ee6af8 4992 if ($elementOffsets.top + $elementDimensions.height + $tooltipDimensions.height - $(document).scrollTop() < $(window).height()) {
4993 var $top = $elementOffsets.top + $elementDimensions.height + 7;
4994 this._tooltip.removeClass('inverse');
4995 $arrow.css('top', -5);
4996 }
4997 else {
08a3a752 4998 var $top = $elementOffsets.top - $tooltipDimensions.height - 7;
c8ee6af8 4999 this._tooltip.addClass('inverse');
5000 $arrow.css('top', $tooltipDimensions.height);
5001 }
9f959ced 5002
e68f4d0d
AE
5003 // calculate left offset
5004 switch ($alignment) {
5005 case 'center':
5006 var $left = Math.round($elementOffsets.left - $tooltipHalfWidth + ($elementDimensions.width / 2));
9f959ced 5007
e68f4d0d
AE
5008 $arrow.css({
5009 left: ($tooltipDimensionsInner.width / 2 - $arrowWidth / 2) + "px"
5010 });
5011 break;
9f959ced 5012
e68f4d0d
AE
5013 case 'left':
5014 var $left = $elementOffsets.left;
9f959ced 5015
e68f4d0d 5016 $arrow.css({
270a205a 5017 left: "5px"
e68f4d0d
AE
5018 });
5019 break;
9f959ced 5020
e68f4d0d
AE
5021 case 'right':
5022 var $left = $elementOffsets.left + $elementDimensions.width - $tooltipDimensions.width;
9f959ced 5023
e68f4d0d 5024 $arrow.css({
270a205a 5025 left: ($tooltipDimensionsInner.width - $arrowWidth - 5) + "px"
e68f4d0d
AE
5026 });
5027 break;
5028 }
9f959ced 5029
e68f4d0d
AE
5030 // move tooltip
5031 this._tooltip.css({
5032 top: $top + "px",
5033 left: $left + "px"
5034 });
5035
5036 // show tooltip
90a8839c 5037 this._tooltip.wcfFadeIn();
2875507b
MW
5038 },
5039
e68f4d0d
AE
5040 /**
5041 * Hides tooltip once cursor left the element.
5042 *
5043 * @param object event
5044 */
2875507b 5045 _mouseLeaveHandler: function(event) {
90a8839c
MW
5046 this._tooltip.stop().hide().css({
5047 opacity: 1
5048 });
c39544e5 5049 }
39e27190 5050});
c39544e5 5051
b3991cb3
AE
5052/**
5053 * Handles clicks outside an overlay, hitting body-tag through bubbling.
5054 *
5055 * You should always remove callbacks before disposing the attached element,
5056 * preventing errors from blocking the iteration. Furthermore you should
5057 * always handle clicks on your overlay's container and return 'false' to
5058 * prevent bubbling.
5059 */
5060WCF.CloseOverlayHandler = {
5061 /**
5062 * list of callbacks
5063 * @var WCF.Dictionary
5064 */
5065 _callbacks: new WCF.Dictionary(),
9f959ced 5066
b3991cb3
AE
5067 /**
5068 * indicates that overlay handler is listening to click events on body-tag
5069 * @var boolean
5070 */
5071 _isListening: false,
9f959ced 5072
b3991cb3
AE
5073 /**
5074 * Adds a new callback.
5075 *
5076 * @param string identifier
5077 * @param object callback
5078 */
5079 addCallback: function(identifier, callback) {
5080 this._bindListener();
83428b7f 5081
b3991cb3 5082 if (this._callbacks.isset(identifier)) {
78e4d558 5083 console.debug("[WCF.CloseOverlayHandler] identifier '" + identifier + "' is already bound to a callback");
b3991cb3
AE
5084 return false;
5085 }
9f959ced 5086
b3991cb3
AE
5087 this._callbacks.add(identifier, callback);
5088 },
9f959ced 5089
b3991cb3
AE
5090 /**
5091 * Removes a callback from list.
5092 *
5093 * @param string identifier
5094 */
5095 removeCallback: function(identifier) {
5096 if (this._callbacks.isset(identifier)) {
5097 this._callbacks.remove(identifier);
5098 }
5099 },
9f959ced 5100
b3991cb3
AE
5101 /**
5102 * Binds click event handler.
5103 */
5104 _bindListener: function() {
5105 if (this._isListening) return;
9f959ced 5106
b3991cb3 5107 $('body').click($.proxy(this._executeCallbacks, this));
9f959ced 5108
b3991cb3
AE
5109 this._isListening = true;
5110 },
9f959ced 5111
b3991cb3
AE
5112 /**
5113 * Executes callbacks on click.
5114 */
83428b7f 5115 _executeCallbacks: function(event) {
b3991cb3
AE
5116 this._callbacks.each(function(pair) {
5117 // execute callback
5118 pair.value();
5119 });
5120 }
5121};
5122
9c1e5045
AE
5123/**
5124 * Notifies objects once a DOM node was inserted.
5125 */
5126WCF.DOMNodeInsertedHandler = {
5127 /**
5128 * list of callbacks
0e6979fb 5129 * @var array<object>
9c1e5045 5130 */
0e6979fb 5131 _callbacks: [ ],
597e2774 5132
9c1e5045
AE
5133 /**
5134 * prevent infinite loop if a callback manipulates DOM
5135 * @var boolean
5136 */
5137 _isExecuting: false,
597e2774 5138
9c1e5045
AE
5139 /**
5140 * Adds a new callback.
5141 *
5142 * @param string identifier
5143 * @param object callback
5144 */
5145 addCallback: function(identifier, callback) {
0e6979fb 5146 this._callbacks.push(callback);
9c1e5045 5147 },
597e2774 5148
9c1e5045
AE
5149 /**
5150 * Executes callbacks on click.
5151 */
d371330f 5152 _executeCallbacks: function() {
42d7d2cc 5153 if (this._isExecuting) return;
597e2774 5154
901393e3 5155 // do not track events while executing callbacks
9c1e5045 5156 this._isExecuting = true;
901393e3 5157
0e6979fb
AE
5158 for (var $i = 0, $length = this._callbacks.length; $i < $length; $i++) {
5159 this._callbacks[$i]();
5160 }
901393e3
AE
5161
5162 // enable listener again
5163 this._isExecuting = false;
597e2774
AE
5164 },
5165
5166 /**
42d7d2cc 5167 * Executes all callbacks.
d371330f 5168 */
42d7d2cc 5169 execute: function() {
d371330f 5170 this._executeCallbacks();
9c1e5045
AE
5171 }
5172};
5173
809e4170
MS
5174/**
5175 * Notifies objects once a DOM node was removed.
5176 */
5177WCF.DOMNodeRemovedHandler = {
5178 /**
5179 * list of callbacks
5180 * @var WCF.Dictionary
5181 */
5182 _callbacks: new WCF.Dictionary(),
9f959ced 5183
809e4170
MS
5184 /**
5185 * prevent infinite loop if a callback manipulates DOM
5186 * @var boolean
5187 */
5188 _isExecuting: false,
9f959ced 5189
809e4170
MS
5190 /**
5191 * indicates that overlay handler is listening to DOMNodeRemoved events on body-tag
5192 * @var boolean
5193 */
5194 _isListening: false,
9f959ced 5195
809e4170
MS
5196 /**
5197 * Adds a new callback.
5198 *
5199 * @param string identifier
5200 * @param object callback
5201 */
5202 addCallback: function(identifier, callback) {
5203 this._bindListener();
9f959ced 5204
809e4170
MS
5205 if (this._callbacks.isset(identifier)) {
5206 console.debug("[WCF.DOMNodeRemovedHandler] identifier '" + identifier + "' is already bound to a callback");
5207 return false;
5208 }
9f959ced 5209
809e4170
MS
5210 this._callbacks.add(identifier, callback);
5211 },
9f959ced 5212
809e4170
MS
5213 /**
5214 * Removes a callback from list.
5215 *
5216 * @param string identifier
5217 */
5218 removeCallback: function(identifier) {
5219 if (this._callbacks.isset(identifier)) {
5220 this._callbacks.remove(identifier);
5221 }
5222 },
9f959ced 5223
809e4170
MS
5224 /**
5225 * Binds click event handler.
5226 */
5227 _bindListener: function() {
5228 if (this._isListening) return;
9f959ced 5229
809e4170 5230 $(document).bind('DOMNodeRemoved', $.proxy(this._executeCallbacks, this));
9f959ced 5231
809e4170
MS
5232 this._isListening = true;
5233 },
9f959ced 5234
809e4170
MS
5235 /**
5236 * Executes callbacks if a DOM node is removed.
5237 */
5238 _executeCallbacks: function(event) {
5239 if (this._isExecuting) return;
9f959ced 5240
809e4170
MS
5241 // do not track events while executing callbacks
5242 this._isExecuting = true;
5243
5244 this._callbacks.each(function(pair) {
5245 // execute callback
5246 pair.value(event);
5247 });
5248
5249 // enable listener again
5250 this._isExecuting = false;
5251 }
5252};
5253
5254/**
5255 * Namespace for table related classes.
5256 */
5257WCF.Table = {};
5258
5259/**
5260 * Handles empty tables which can be used in combination with WCF.Action.Proxy.
5261 */
5262WCF.Table.EmptyTableHandler = Class.extend({
5263 /**
5264 * handler options
5265 * @var object
5266 */
5267 _options: {},
5268
5269 /**
5270 * class name of the relevant rows
5271 * @var string
5272 */
5273 _rowClassName: '',
5274
5275 /**
5276 * Initalizes a new WCF.Table.EmptyTableHandler object.
5277 *
809e4170 5278 * @param jQuery tableContainer
e0ee2eff 5279 * @param string rowClassName
809e4170
MS
5280 * @param object options
5281 */
e0ee2eff 5282 init: function(tableContainer, rowClassName, options) {
809e4170
MS
5283 this._rowClassName = rowClassName;
5284 this._tableContainer = tableContainer;
5285
5286 this._options = $.extend(true, {
5287 emptyMessage: null,
c0bd9c8a 5288 messageType: 'info',
809e4170
MS
5289 refreshPage: false,
5290 updatePageNumber: false
5291 }, options || { });
5292
5293 WCF.DOMNodeRemovedHandler.addCallback('WCF.Table.EmptyTableHandler.' + rowClassName, $.proxy(this._remove, this));
5294 },
5295
5296 /**
5297 * Handles the removal of a DOM node.
5298 */
5299 _remove: function(event) {
5300 var element = $(event.target);
5301
5302 // check if DOM element is relevant
5303 if (element.hasClass(this._rowClassName)) {
5304 var tbody = element.parents('tbody:eq(0)');
5305
5306 // check if table will be empty if DOM node is removed
5307 if (tbody.children('tr').length == 1) {
5308 if (this._options.emptyMessage) {
5309 // insert message
5310 this._tableContainer.replaceWith($('<p />').addClass(this._options.messageType).text(this._options.emptyMessage));
5311 }
5312 else if (this._options.refreshPage) {
5313 // refresh page
5314 if (this._options.updatePageNumber) {
5315 // calculate the new page number
5316 var pageNumberURLComponents = window.location.href.match(/(\?|&)pageNo=(\d+)/g);
5317 if (pageNumberURLComponents) {
5318 var currentPageNumber = pageNumberURLComponents[pageNumberURLComponents.length - 1].match(/\d+/g);
5319 if (this._options.updatePageNumber > 0) {
5320 currentPageNumber++;
5321 }
5322 else {
5323 currentPageNumber--;
5324 }
5325
5326 window.location = window.location.href.replace(pageNumberURLComponents[pageNumberURLComponents.length - 1], pageNumberURLComponents[pageNumberURLComponents.length - 1][0] + 'pageNo=' + currentPageNumber);
5327 }
5328 }
5329 else {
5330 window.location.reload();
5331 }
5332 }
5333 else {
5334 // simply remove the table container
5335 this._tableContainer.remove();
5336 }
5337 }
5338 }
5339 }
5340});
5341
046e1292
AE
5342/**
5343 * Namespace for search related classes.
5344 */
5345WCF.Search = {};
5346
5347/**
4ad00d80 5348 * Performs a quick search.
046e1292 5349 */
4ad00d80 5350WCF.Search.Base = Class.extend({
046e1292
AE
5351 /**
5352 * notification callback
5353 * @var object
5354 */
5355 _callback: null,
4ad00d80 5356
4d10750a
AE
5357 /**
5358 * class name
5359 * @var string
5360 */
4ad00d80 5361 _className: '',
c000b08a 5362
cf55c379
AE
5363 /**
5364 * comma seperated list
5365 * @var boolean
5366 */
5367 _commaSeperated: false,
5368
c000b08a
MS
5369 /**
5370 * list with values that are excluded from seaching
5371 * @var array
5372 */
5373 _excludedSearchValues: [],
cf55c379 5374
9a130ab4
AE
5375 /**
5376 * count of available results
5377 * @var integer
5378 */
5379 _itemCount: 0,
5380
5381 /**
5382 * item index, -1 if none is selected
5383 * @var integer
5384 */
5385 _itemIndex: -1,
5386
046e1292
AE
5387 /**
5388 * result list
5389 * @var jQuery
5390 */
5391 _list: null,
cf55c379
AE
5392
5393 /**
5394 * old search string, used for comparison
5395 * @var array<string>
5396 */
5397 _oldSearchString: [ ],
5398
046e1292
AE
5399 /**
5400 * action proxy
5401 * @var WCF.Action.Proxy
5402 */
5403 _proxy: null,
cf55c379 5404
046e1292
AE
5405 /**
5406 * search input field
5407 * @var jQuery
5408 */
5409 _searchInput: null,
4d10750a
AE
5410
5411 /**
5412 * minimum search input length, MUST be 1 or higher
5413 * @var integer
5414 */
fbe99f89 5415 _triggerLength: 3,
cf55c379 5416
046e1292
AE
5417 /**
5418 * Initializes a new search.
5419 *
5420 * @param jQuery searchInput
5421 * @param object callback
c000b08a 5422 * @param array excludedSearchValues
cf55c379 5423 * @param boolean commaSeperated
80da9de3 5424 * @param boolean showLoadingOverlay
046e1292 5425 */
80da9de3 5426 init: function(searchInput, callback, excludedSearchValues, commaSeperated, showLoadingOverlay) {
b0ce5298 5427 if (callback !== null && callback !== undefined && !$.isFunction(callback)) {
6f475a52 5428 console.debug("[WCF.Search.Base] The given callback is invalid, aborting.");
046e1292
AE
5429 return;
5430 }
b0ce5298
AE
5431
5432 this._callback = (callback) ? callback : null;
fdd8763a 5433 this._excludedSearchValues = [];
c000b08a
MS
5434 if (excludedSearchValues) {
5435 this._excludedSearchValues = excludedSearchValues;
5436 }
71662ae8
AE
5437
5438 this._searchInput = $(searchInput);
5439 if (!this._searchInput.length) {
5440 console.debug("[WCF.Search.Base] Selector '" + searchInput + "' for search input is invalid, aborting.");
5441 return;
5442 }
5443
fbdfbaba 5444 this._searchInput.keydown($.proxy(this._keyDown, this)).keyup($.proxy(this._keyUp, this)).wrap('<span class="dropdown" />');
7f45f320 5445 this._list = $('<ul class="dropdownMenu" />').insertAfter(this._searchInput);
cf55c379
AE
5446 this._commaSeperated = (commaSeperated) ? true : false;
5447 this._oldSearchString = [ ];
046e1292 5448
9a130ab4
AE
5449 this._itemCount = 0;
5450 this._itemIndex = -1;
5451
046e1292 5452 this._proxy = new WCF.Action.Proxy({
387cd7da 5453 showLoadingOverlay: (showLoadingOverlay !== true ? false : true),
40b173ec
MK
5454 success: $.proxy(this._success, this),
5455 autoAbortPrevious: true
046e1292 5456 });
b0ce5298 5457
404c9abe 5458 if (this._searchInput.is('input')) {
b0ce5298
AE
5459 this._searchInput.attr('autocomplete', 'off');
5460 }
03e34cc3
AE
5461
5462 this._searchInput.blur($.proxy(this._blur, this));
38d131ce
AE
5463
5464 WCF.Dropdown.initDropdownFragment(this._searchInput.parent(), this._list);
03e34cc3
AE
5465 },
5466
5467 /**
5468 * Closes the dropdown after a short delay.
5469 */
5470 _blur: function() {
5471 var self = this;
5472 new WCF.PeriodicalExecuter(function(pe) {
5473 if (self._list.is(':visible')) {
5474 self._clearList(false);
5475 }
5476
5477 pe.stop();
369b47cd 5478 }, 250);
046e1292 5479 },
4ad00d80 5480
fbdfbaba
AE
5481 /**
5482 * Blocks execution of 'Enter' event.
5483 *
5484 * @param object event
5485 */
5486 _keyDown: function(event) {
7c6f7523
AE
5487 if (event.which === $.ui.keyCode.ENTER) {
5488 var $dropdown = this._searchInput.parents('.dropdown');
5489
5490 if ($dropdown.data('disableAutoFocus')) {
5491 if (this._itemIndex !== -1) {
5492 event.preventDefault();
5493 }
c138555b 5494 }
7c6f7523 5495 else if ($dropdown.data('preventSubmit') || this._itemIndex !== -1) {
c138555b
AE
5496 event.preventDefault();
5497 }
fbdfbaba
AE
5498 }
5499 },
5500
046e1292
AE
5501 /**
5502 * Performs a search upon key up.
cf55c379
AE
5503 *
5504 * @param object event
046e1292 5505 */
cf55c379 5506 _keyUp: function(event) {
9a130ab4
AE
5507 // handle arrow keys and return key
5508 switch (event.which) {
5509 case 37: // arrow-left
5510 case 39: // arrow-right
5511 return;
5512 break;
5513
5514 case 38: // arrow up
5515 this._selectPreviousItem();
5516 return;
5517 break;
5518
5519 case 40: // arrow down
5520 this._selectNextItem();
5521 return;
5522 break;
5523
5524 case 13: // return key
5525 return this._selectElement(event);
5526 break;
5527 }
5528
cf55c379 5529 var $content = this._getSearchString(event);
046e1292
AE
5530 if ($content === '') {
5531 this._clearList(true);
5532 }
4d10750a 5533 else if ($content.length >= this._triggerLength) {
4ad00d80
AE
5534 var $parameters = {
5535 data: {
c000b08a 5536 excludedSearchValues: this._excludedSearchValues,
4ad00d80 5537 searchString: $content
596a5751 5538 }
4ad00d80
AE
5539 };
5540
b608ea34 5541 this._searchInput.parents('.searchBar').addClass('loading');
046e1292 5542 this._proxy.setOption('data', {
a427a8c8 5543 actionName: 'getSearchResultList',
4ad00d80 5544 className: this._className,
f005c2c9 5545 interfaceName: 'wcf\\data\\ISearchAction',
4ad00d80 5546 parameters: this._getParameters($parameters)
046e1292
AE
5547 });
5548 this._proxy.sendRequest();
5549 }
4d10750a
AE
5550 else {
5551 // input below trigger length
5552 this._clearList(false);
5553 }
046e1292 5554 },
4ad00d80 5555
9a130ab4
AE
5556 /**
5557 * Selects the next item in list.
5558 */
5559 _selectNextItem: function() {
5560 if (this._itemCount === 0) {
5561 return;
5562 }
5563
5564 // remove previous marking
5565 this._itemIndex++;
5566 if (this._itemIndex === this._itemCount) {
5567 this._itemIndex = 0;
5568 }
5569
5570 this._highlightSelectedElement();
5571 },
5572
5573 /**
5574 * Selects the previous item in list.
5575 */
5576 _selectPreviousItem: function() {
5577 if (this._itemCount === 0) {
5578 return;
5579 }
5580
5581 this._itemIndex--;
5582 if (this._itemIndex === -1) {
5583 this._itemIndex = this._itemCount - 1;
5584 }
5585
5586 this._highlightSelectedElement();
5587 },
5588
5589 /**
5590 * Highlights the active item.
5591 */
5592 _highlightSelectedElement: function() {
5593 this._list.find('li').removeClass('dropdownNavigationItem');
5594 this._list.find('li:eq(' + this._itemIndex + ')').addClass('dropdownNavigationItem');
5595 },
5596
5597 /**
5598 * Selects the active item by pressing the return key.
5599 *
5600 * @param object event
5601 * @return boolean
5602 */
5603 _selectElement: function(event) {
5604 if (this._itemCount === 0) {
5605 return true;
5606 }
5607
5608 this._list.find('li.dropdownNavigationItem').trigger('click');
5609
5610 return false;
5611 },
5612
cf55c379
AE
5613 /**
5614 * Returns search string.
5615 *
5616 * @return string
5617 */
5618 _getSearchString: function(event) {
5619 var $searchString = $.trim(this._searchInput.val());
5620 if (this._commaSeperated) {
5621 var $keyCode = event.keyCode || event.which;
5622 if ($keyCode == 188) {
5623 // ignore event if char is 188 = ,
5624 return '';
5625 }
5626
5627 var $current = $searchString.split(',');
68097635
AE
5628 var $length = $current.length;
5629 for (var $i = 0; $i < $length; $i++) {
cf55c379
AE
5630 // remove whitespaces at the beginning or end
5631 $current[$i] = $.trim($current[$i]);
68097635
AE
5632 }
5633
5634 for (var $i = 0; $i < $length; $i++) {
cf55c379
AE
5635 var $part = $current[$i];
5636
5637 if (this._oldSearchString[$i]) {
5638 // compare part
5639 if ($part != this._oldSearchString[$i]) {
5640 // current part was changed
5641 $searchString = $part;
5642 break;
5643 }
5644 }
5645 else {
5646 // new part was added
5647 $searchString = $part;
5648 break;
5649 }
5650 }
5651
5652 this._oldSearchString = $current;
5653 }
5654
5655 return $searchString;
5656 },
5657
4ad00d80
AE
5658 /**
5659 * Returns parameters for quick search.
5660 *
5661 * @param object parameters
5662 * @return object
5663 */
5664 _getParameters: function(parameters) {
5665 return parameters;
5666 },
9f959ced 5667
046e1292
AE
5668 /**
5669 * Evalutes search results.
5670 *
5671 * @param object data
5672 * @param string textStatus
5673 * @param jQuery jqXHR
5674 */
5675 _success: function(data, textStatus, jqXHR) {
7f45f320 5676 this._clearList(false);
b608ea34 5677 this._searchInput.parents('.searchBar').removeClass('loading');
7f45f320 5678
c138555b
AE
5679 if ($.getLength(data.returnValues)) {
5680 for (var $i in data.returnValues) {
5681 var $item = data.returnValues[$i];
5682
5683 this._createListItem($item);
5684 }
046e1292 5685 }
c138555b
AE
5686 else if (!this._handleEmptyResult()) {
5687 return;
046e1292 5688 }
4ad00d80 5689
1af138b0
AE
5690 WCF.CloseOverlayHandler.addCallback('WCF.Search.Base', $.proxy(function() { this._clearList(); }, this));
5691
2d305fef
AE
5692 var $containerID = this._searchInput.parents('.dropdown').wcfIdentify();
5693 if (!WCF.Dropdown.getDropdownMenu($containerID).hasClass('dropdownOpen')) {
5694 WCF.Dropdown.toggleDropdown($containerID);
114bb112
AE
5695 }
5696
1af138b0 5697 // pre-select first item
4765dbfd 5698 this._itemIndex = -1;
2d305fef
AE
5699 if (!WCF.Dropdown.getDropdown($containerID).data('disableAutoFocus')) {
5700 this._selectNextItem();
5701 }
046e1292 5702 },
4ad00d80 5703
c138555b
AE
5704 /**
5705 * Handles empty result lists, should return false if dropdown should be hidden.
5706 *
5707 * @return boolean
5708 */
5709 _handleEmptyResult: function() {
5710 return false;
5711 },
5712
4ad00d80
AE
5713 /**
5714 * Creates a new list item.
5715 *
5716 * @param object item
5717 * @return jQuery
5718 */
5719 _createListItem: function(item) {
83fccf43 5720 var $listItem = $('<li><span>' + WCF.String.escapeHTML(item.label) + '</span></li>').appendTo(this._list);
1c93e6e7 5721 $listItem.data('objectID', item.objectID).data('label', item.label).click($.proxy(this._executeCallback, this));
4ad00d80 5722
9a130ab4
AE
5723 this._itemCount++;
5724
4ad00d80
AE
5725 return $listItem;
5726 },
9f959ced 5727
046e1292
AE
5728 /**
5729 * Executes callback upon result click.
5730 *
5731 * @param object event
5732 */
5733 _executeCallback: function(event) {
b0ce5298 5734 var $clearSearchInput = false;
046e1292 5735 var $listItem = $(event.currentTarget);
046e1292 5736 // notify callback
cf55c379
AE
5737 if (this._commaSeperated) {
5738 // auto-complete current part
5739 var $result = $listItem.data('label');
5740 for (var $i = 0, $length = this._oldSearchString.length; $i < $length; $i++) {
5741 var $part = this._oldSearchString[$i];
47573ca9 5742 if ($result.toLowerCase().indexOf($part.toLowerCase()) === 0) {
cf55c379 5743 this._oldSearchString[$i] = $result;
cf1ca3a5 5744 this._searchInput.val(this._oldSearchString.join(', '));
cf55c379
AE
5745
5746 if ($.browser.webkit) {
5747 // chrome won't display the new value until the textarea is rendered again
5748 // this quick fix forces chrome to render it again, even though it changes nothing
5749 this._searchInput.css({ display: 'block' });
5750 }
5751
68097635 5752 // set focus on input field again
47573ca9 5753 var $position = this._searchInput.val().toLowerCase().indexOf($result.toLowerCase()) + $result.length;
68097635
AE
5754 this._searchInput.focus().setCaret($position);
5755
cf55c379
AE
5756 break;
5757 }
5758 }
5759 }
5760 else {
b0ce5298
AE
5761 if (this._callback === null) {
5762 this._searchInput.val($listItem.data('label'));
5763 }
5764 else {
5765 $clearSearchInput = (this._callback($listItem.data()) === true) ? true : false;
5766 }
cf55c379 5767 }
9f959ced 5768
046e1292 5769 // close list and revert input
f28efd50 5770 this._clearList($clearSearchInput);
046e1292 5771 },
9f959ced 5772
046e1292
AE
5773 /**
5774 * Closes the suggestion list and clears search input on demand.
5775 *
5776 * @param boolean clearSearchInput
5777 */
5778 _clearList: function(clearSearchInput) {
cf55c379 5779 if (clearSearchInput && !this._commaSeperated) {
046e1292
AE
5780 this._searchInput.val('');
5781 }
9f959ced 5782
596a5751
MS
5783 // close dropdown
5784 WCF.Dropdown.getDropdown(this._searchInput.parents('.dropdown').wcfIdentify()).removeClass('dropdownOpen');
5785 WCF.Dropdown.getDropdownMenu(this._searchInput.parents('.dropdown').wcfIdentify()).removeClass('dropdownOpen');
5786
5787 this._list.end().empty();
7f45f320
AE
5788
5789 WCF.CloseOverlayHandler.removeCallback('WCF.Search.Base');
9a130ab4
AE
5790
5791 // reset item navigation
5792 this._itemCount = 0;
5793 this._itemIndex = -1;
c000b08a
MS
5794 },
5795
5796 /**
5797 * Adds an excluded search value.
5798 *
5799 * @param string value
5800 */
5801 addExcludedSearchValue: function(value) {
5802 if (!WCF.inArray(value, this._excludedSearchValues)) {
5803 this._excludedSearchValues.push(value);
5804 }
5805 },
5806
5807 /**
0b17ce2f 5808 * Removes an excluded search value.
c000b08a
MS
5809 *
5810 * @param string value
5811 */
5812 removeExcludedSearchValue: function(value) {
5813 var index = $.inArray(value, this._excludedSearchValues);
5814 if (index != -1) {
5815 this._excludedSearchValues.splice(index, 1);
5816 }
046e1292
AE
5817 }
5818});
5819
4ad00d80
AE
5820/**
5821 * Provides quick search for users and user groups.
5822 *
5823 * @see WCF.Search.Base
5824 */
5825WCF.Search.User = WCF.Search.Base.extend({
5826 /**
5827 * @see WCF.Search.Base._className
5828 */
5829 _className: 'wcf\\data\\user\\UserAction',
5830
5831 /**
5832 * include user groups in search
5833 * @var boolean
5834 */
5835 _includeUserGroups: false,
5836
5837 /**
cf55c379 5838 * @see WCF.Search.Base.init()
4ad00d80 5839 */
cf55c379 5840 init: function(searchInput, callback, includeUserGroups, excludedSearchValues, commaSeperated) {
4ad00d80
AE
5841 this._includeUserGroups = includeUserGroups;
5842
cf55c379 5843 this._super(searchInput, callback, excludedSearchValues, commaSeperated);
4ad00d80
AE
5844 },
5845
5846 /**
5847 * @see WCF.Search.Base._getParameters()
5848 */
5849 _getParameters: function(parameters) {
4ccbb0bb 5850 parameters.data.includeUserGroups = this._includeUserGroups ? 1 : 0;
1c93e6e7
AE
5851
5852 return parameters;
4ad00d80
AE
5853 },
5854
5855 /**
5856 * @see WCF.Search.Base._createListItem()
5857 */
5858 _createListItem: function(item) {
5859 var $listItem = this._super(item);
5860
c2d0b2d6
MS
5861 var $icon = null;
5862 if (item.icon) {
5863 $icon = $(item.icon);
5864 }
5865 else if (this._includeUserGroups && item.type === 'group') {
5866 $icon = $('<span class="icon icon16 icon-group" />');
5867 }
5868
5869 if ($icon) {
5870 var $label = $listItem.find('span').detach();
5871
5872 var $box16 = $('<div />').addClass('box16').appendTo($listItem);
5873
32811021 5874 $box16.append($icon);
c2d0b2d6
MS
5875 $box16.append($('<div />').append($label));
5876 }
5877
4ad00d80 5878 // insert item type
4ad00d80
AE
5879 $listItem.data('type', item.type);
5880
5881 return $listItem;
5882 }
5883});
5884
5568e009
AE
5885/**
5886 * Namespace for system-related classes.
5887 */
5888WCF.System = { };
5889
eb1537e3
AE
5890/**
5891 * Namespace for dependency-related classes.
5892 */
5893WCF.System.Dependency = { };
5894
5895/**
5896 * JavaScript Dependency Manager.
5897 */
5898WCF.System.Dependency.Manager = {
5899 /**
5900 * list of callbacks grouped by identifier
5901 * @var object
5902 */
5903 _callbacks: { },
5904
5905 /**
5906 * list of loaded identifiers
5907 * @var array<string>
5908 */
5909 _loaded: [ ],
5910
5911 /**
5912 * list of setup callbacks grouped by identifier
5913 * @var object
5914 */
5915 _setupCallbacks: { },
5916
5917 /**
5918 * Registers a callback for given identifier, will be executed after all setup
5919 * callbacks have been invoked.
5920 *
5921 * @param string identifier
5922 * @param object callback
5923 */
5924 register: function(identifier, callback) {
5925 if (!$.isFunction(callback)) {
5926 console.debug("[WCF.System.Dependency.Manager] Callback for identifier '" + identifier + "' is invalid, aborting.");
5927 return;
5928 }
5929
5930 // already loaded, invoke now
5931 if (WCF.inArray(identifier, this._loaded)) {
9fb5eeac
AE
5932 setTimeout(function() {
5933 callback();
5934 }, 1);
eb1537e3
AE
5935 }
5936 else {
5937 if (!this._callbacks[identifier]) {
5938 this._callbacks[identifier] = [ ];
5939 }
5940
5941 this._callbacks[identifier].push(callback);
5942 }
5943 },
5944
5945 /**
5946 * Registers a setup callback for given identifier, will be invoked
5947 * prior to all other callbacks.
5948 *
5949 * @param string identifier
5950 * @param object callback
5951 */
5952 setup: function(identifier, callback) {
5953 if (!$.isFunction(callback)) {
5954 console.debug("[WCF.System.Dependency.Manager] Setup callback for identifier '" + identifier + "' is invalid, aborting.");
5955 return;
5956 }
5957
5958 if (!this._setupCallbacks[identifier]) {
5959 this._setupCallbacks[identifier] = [ ];
5960 }
5961
5962 this._setupCallbacks[identifier].push(callback);
5963 },
5964
5965 /**
5966 * Invokes all callbacks for given identifier and marks it as loaded.
5967 *
5968 * @param string identifier
5969 */
5970 invoke: function(identifier) {
5971 if (this._setupCallbacks[identifier]) {
5972 for (var $i = 0, $length = this._setupCallbacks[identifier].length; $i < $length; $i++) {
5973 this._setupCallbacks[identifier][$i]();
5974 }
2f51337e
AE
5975
5976 delete this._setupCallbacks[identifier];
eb1537e3
AE
5977 }
5978
5979 this._loaded.push(identifier);
5980
5981 if (this._callbacks[identifier]) {
5982 for (var $i = 0, $length = this._callbacks[identifier].length; $i < $length; $i++) {
5983 this._callbacks[identifier][$i]();
5984 }
2f51337e
AE
5985
5986 delete this._callbacks[identifier];
eb1537e3
AE
5987 }
5988 }
5989};
5990
88c8e868 5991/**
7b608580 5992 * Provides flexible dropdowns for tab-based menus.
88c8e868 5993 */
7b608580
AE
5994WCF.System.FlexibleMenu = {
5995 /**
5996 * list of containers
5997 * @var object<jQuery>
5998 */
5999 _containers: { },
6000
6001 /**
6002 * list of registered container ids
6003 * @var array<string>
6004 */
6005 _containerIDs: [ ],
6006
6007 /**
6008 * list of dropdowns
6009 * @var object<jQuery>
6010 */
6011 _dropdowns: { },
6012
6013 /**
6014 * list of dropdown menus
6015 * @var object<jQuery>
6016 */
6017 _dropdownMenus: { },
6018
6019 /**
6020 * list of hidden status for containers
6021 * @var object<boolean>
6022 */
6023 _hasHiddenItems: { },
6024
6025 /**
6026 * true if menus are currently rebuilt
6027 * @var boolean
6028 */
6029 _isWorking: false,
6030
6031 /**
6032 * list of tab menu items per container
6033 * @var object<jQuery>
6034 */
6035 _menuItems: { },
6036
6037 /**
6038 * Initializes the WCF.System.FlexibleMenu class.
6039 */
88c8e868 6040 init: function() {
7b608580
AE
6041 // register .mainMenu and .navigationHeader by default
6042 this.registerMenu('mainMenu');
6043 this.registerMenu($('.navigationHeader:eq(0)').wcfIdentify());
88c8e868 6044
8e514e3f
AE
6045 this._registerTabMenus();
6046
7b608580 6047 $(window).resize($.proxy(this.rebuildAll, this));
8e514e3f
AE
6048 WCF.DOMNodeInsertedHandler.addCallback('WCF.System.FlexibleMenu', $.proxy(this._registerTabMenus, this));
6049 },
6050
6051 /**
6052 * Registers tab menus.
6053 */
6054 _registerTabMenus: function() {
6055 // register tab menus
6056 $('.tabMenuContainer:not(.jsFlexibleMenuEnabled)').each(function(index, tabMenuContainer) {
6057 var $navigation = $(tabMenuContainer).children('nav');
6058 if ($navigation.length && $navigation.find('> ul:eq(0) > li').length) {
6059 WCF.System.FlexibleMenu.registerMenu($navigation.wcfIdentify());
6060 }
6061 });
88c8e868
MW
6062 },
6063
7b608580
AE
6064 /**
6065 * Registers a tab-based menu by id.
6066 *
6067 * Required DOM:
6068 * <container>
6069 * <ul style="white-space: nowrap">
6070 * <li>tab 1</li>
6071 * <li>tab 2</li>
6072 * ...
6073 * <li>tab n</li>
6074 * </ul>
6075 * </container>
6076 *
6077 * @param string containerID
6078 */
6079 registerMenu: function(containerID) {
6080 var $container = $('#' + containerID);
6081 if (!$container.length) {
6082 console.debug("[WCF.System.FlexibleMenu] Unable to find container identified by '" + containerID + "', aborting.");
6083 return;
6084 }
6085
6086 this._containerIDs.push(containerID);
6087 this._containers[containerID] = $container;
6088 this._menuItems[containerID] = $container.find('> ul:eq(0) > li');
67fde3bf 6089 this._dropdowns[containerID] = $('<li class="dropdown"><a class="icon icon16 icon-list" /></li>').data('containerID', containerID).click($.proxy(this._click, this));
7b608580
AE
6090 this._dropdownMenus[containerID] = $('<ul class="dropdownMenu" />').appendTo(this._dropdowns[containerID]);
6091 this._hasHiddenItems[containerID] = false;
6092
6093 this.rebuild(containerID);
6094
6095 WCF.Dropdown.initDropdown(this._dropdowns[containerID].children('a'));
6096 },
6097
6098 /**
6099 * Rebuilds all registered containers.
6100 */
6101 rebuildAll: function() {
6102 if (this._isWorking) {
6103 return;
6104 }
6105
6106 this._isWorking = true;
6107
6108 for (var $i = 0, $length = this._containerIDs.length; $i < $length; $i++) {
6109 this.rebuild(this._containerIDs[$i]);
6110 }
6111
6112 this._isWorking = false;
6113 },
6114
6115 /**
6116 * Rebuilds a container, will be automatically invoked on window resize and registering.
6117 *
6118 * @param string containerID
6119 */
6120 rebuild: function(containerID) {
6121 if (!this._containers[containerID]) {
6122 console.debug("[WCF.System.FlexibleMenu] Cannot rebuild unknown container identified by '" + containerID + "'");
6123 return;
6124 }
6125
6126 var $changedItems = false;
377a49cc 6127 var $container = this._containers[containerID];
7b608580
AE
6128 var $currentWidth = 0;
6129
6130 // the current width is based upon all items without the dropdown
6131 var $menuItems = this._menuItems[containerID].filter(':visible');
6132 for (var $i = 0, $length = $menuItems.length; $i < $length; $i++) {
6133 $currentWidth += $($menuItems[$i]).outerWidth(true);
6134 }
6135
377a49cc
AE
6136 // insert dropdown for calculation purposes
6137 if (!this._hasHiddenItems[containerID]) {
6138 this._dropdowns[containerID].appendTo($container.children('ul:eq(0)'));
6139 }
6140
7b608580 6141 var $dropdownWidth = this._dropdowns[containerID].outerWidth(true);
377a49cc
AE
6142
6143 // remove dropdown previously inserted
6144 if (!this._hasHiddenItems[containerID]) {
6145 this._dropdowns[containerID].detach();
6146 }
6147
8960a235 6148 var $maximumWidth = $container.parent().innerWidth();
7b608580
AE
6149
6150 // substract padding from the parent element
6151 $maximumWidth -= parseInt($container.parent().css('padding-left').replace(/px$/, '')) + parseInt($container.parent().css('padding-right').replace(/px$/, ''));
6152
6153 // substract margins and paddings from the container itself
6154 $maximumWidth -= parseInt($container.css('margin-left').replace(/px$/, '')) + parseInt($container.css('margin-right').replace(/px$/, ''));
6155 $maximumWidth -= parseInt($container.css('padding-left').replace(/px$/, '')) + parseInt($container.css('padding-right').replace(/px$/, ''));
6156
6157 // substract paddings from the actual list
6158 $maximumWidth -= parseInt($container.children('ul:eq(0)').css('padding-left').replace(/px$/, '')) + parseInt($container.children('ul:eq(0)').css('padding-right').replace(/px$/, ''));
8960a235
AE
6159 if ($currentWidth > $maximumWidth || (this._hasHiddenItems[containerID] && ($currentWidth > $maximumWidth - $dropdownWidth))) {
6160 var $menuItems = $menuItems.filter(':not(.active):not(.ui-state-active):visible');
6161
6162 // substract dropdown width from maximum width
6163 $maximumWidth -= $dropdownWidth;
7b608580
AE
6164
6165 // hide items starting with the last in list (ignores active item)
6166 for (var $i = ($menuItems.length - 1); $i >= 0; $i--) {
6167 if ($currentWidth > $maximumWidth) {
6168 var $item = $($menuItems[$i]);
6169 $currentWidth -= $item.outerWidth(true);
6170 $item.hide();
6171
6172 $changedItems = true;
6173 this._hasHiddenItems[containerID] = true;
6174 }
6175 else {
6176 break;
6177 }
6178 }
6179
6180 if (this._hasHiddenItems[containerID]) {
67fde3bf 6181 this._dropdowns[containerID].appendTo($container.children('ul:eq(0)'));
7b608580
AE
6182 }
6183 }
6184 else if (this._hasHiddenItems[containerID] && $currentWidth < $maximumWidth) {
6185 var $hiddenItems = this._menuItems[containerID].filter(':not(:visible)');
6186
8960a235
AE
6187 // substract dropdown width from maximum width unless it is the last item
6188 $maximumWidth -= $dropdownWidth;
6189
7b608580
AE
6190 // reverts items starting with the first hidden one
6191 for (var $i = 0, $length = $hiddenItems.length; $i < $length; $i++) {
6192 var $item = $($hiddenItems[$i]);
6193 $currentWidth += $item.outerWidth();
8960a235
AE
6194
6195 if ($i + 1 == $length) {
6196 $maximumWidth += $dropdownWidth;
6197 }
6198
7b608580
AE
6199 if ($currentWidth < $maximumWidth) {
6200 // enough space, show item
6201 $item.css('display', '');
6202 $changedItems = true;
6203 }
6204 else {
6205 break;
6206 }
6207 }
6208
6209 if ($changedItems) {
6210 this._hasHiddenItems[containerID] = (this._menuItems[containerID].filter(':not(:visible)').length > 0);
6211 if (!this._hasHiddenItems[containerID]) {
67fde3bf 6212 this._dropdowns[containerID].detach();
7b608580
AE
6213 }
6214 }
6215 }
6216
6217 // build dropdown menu for hidden items
6218 if ($changedItems) {
6219 this._dropdownMenus[containerID].empty();
6220 this._menuItems[containerID].filter(':not(:visible)').each($.proxy(function(index, item) {
6221 $('<li>' + $(item).html() + '</li>').appendTo(this._dropdownMenus[containerID]);
6222 }, this));
6223 }
88c8e868
MW
6224 }
6225};
6226
23192d23
AE
6227/**
6228 * Namespace for mobile device-related classes.
6229 */
6230WCF.System.Mobile = { };
6231
6232/**
6233 * Handles general navigation and UX on mobile devices.
6234 */
6235WCF.System.Mobile.UX = {
41385e13
AE
6236 /**
6237 * true if mobile optimizations are enabled
6238 * @var boolean
6239 */
6240 _enabled: false,
6241
23192d23
AE
6242 /**
6243 * main container
6244 * @var jQuery
6245 */
6246 _main: null,
6247
41385e13
AE
6248 /**
6249 * sidebar container
6250 * @var jQuery
6251 */
6252 _sidebar: null,
6253
23192d23
AE
6254 /**
6255 * Initializes the WCF.System.Mobile.UX class.
6256 */
6257 init: function() {
41385e13 6258 this._enabled = false;
23192d23 6259 this._main = $('#main');
41385e13 6260 this._sidebar = this._main.find('> div > div > .sidebar');
23192d23 6261
41385e13
AE
6262 if ($.browser.touch) {
6263 $('html').addClass('touch');
6264 }
6265
6266 enquire.register('screen and (max-width: 800px)', {
6267 match: $.proxy(this._enable, this),
6268 unmatch: $.proxy(this._disable, this),
6269 setup: $.proxy(this._setup, this),
6270 deferSetup: true
6271 });
23192d23 6272
41385e13
AE
6273 if ($.browser.msie && this._sidebar.width() > 305) {
6274 // sidebar is rarely broken on IE9/IE10
6275 this._sidebar.css('display', 'none').css('display', '');
6276 }
6277 },
6278
6279 /**
6280 * Initializes the mobile optimization once the media query matches.
6281 */
6282 _setup: function() {
23192d23
AE
6283 this._initSidebarToggleButtons();
6284 this._initSearchBar();
6285 this._initButtonGroupNavigation();
6286
f3e114a1 6287 WCF.CloseOverlayHandler.addCallback('WCF.System.Mobile.UX', $.proxy(this._closeMenus, this));
23192d23
AE
6288 WCF.DOMNodeInsertedHandler.addCallback('WCF.System.Mobile.UX', $.proxy(this._initButtonGroupNavigation, this));
6289 },
6290
41385e13
AE
6291 /**
6292 * Enables the mobile optimization.
6293 */
6294 _enable: function() {
6295 this._enabled = true;
6296
6297 if ($.browser.msie) {
6298 this._sidebar.css('display', 'none').css('display', '');
6299 }
6300 },
6301
6302 /**
6303 * Disables the mobile optimization.
6304 */
6305 _disable: function() {
6306 this._enabled = false;
6307
6308 if ($.browser.msie) {
6309 this._sidebar.css('display', 'none').css('display', '');
6310 }
6311 },
6312
23192d23
AE
6313 /**
6314 * Initializes the sidebar toggle buttons.
6315 */
6316 _initSidebarToggleButtons: function() {
6317 var $sidebarLeft = this._main.hasClass('sidebarOrientationLeft');
6318 var $sidebarRight = this._main.hasClass('sidebarOrientationRight');
6319 if ($sidebarLeft || $sidebarRight) {
6320 // use icons if language item is empty/non-existant
6321 var $languageShowSidebar = 'wcf.global.sidebar.show' + ($sidebarLeft ? 'Left' : 'Right') + 'Sidebar';
6322 if ($languageShowSidebar === WCF.Language.get($languageShowSidebar) || WCF.Language.get($languageShowSidebar) === '') {
6323 $languageShowSidebar = '<span class="icon icon16 icon-double-angle-' + ($sidebarLeft ? 'left' : 'right') + '" />';
6324 }
6325
6326 var $languageHideSidebar = 'wcf.global.sidebar.hide' + ($sidebarLeft ? 'Left' : 'Right') + 'Sidebar';
6327 if ($languageHideSidebar === WCF.Language.get($languageHideSidebar) || WCF.Language.get($languageHideSidebar) === '') {
6328 $languageHideSidebar = '<span class="icon icon16 icon-double-angle-' + ($sidebarLeft ? 'right' : 'left') + '" />';
6329 }
6330
6331 // add toggle buttons
6332 var self = this;
6333 $('<span class="button small mobileSidebarToggleButton">' + $languageShowSidebar + '</span>').appendTo($('.content')).click(function() { self._main.addClass('mobileShowSidebar'); });
6334 $('<span class="button small mobileSidebarToggleButton">' + $languageHideSidebar + '</span>').appendTo($('.sidebar')).click(function() { self._main.removeClass('mobileShowSidebar'); });
6335 }
6336 },
6337
6338 /**
6339 * Initializes the search bar.
6340 */
6341 _initSearchBar: function() {
6342 var $searchBar = $('.searchBar:eq(0)');
6343
41385e13
AE
6344 var self = this;
6345 $searchBar.click(function() {
6346 if (self._enabled) {
6347 $searchBar.addClass('searchBarOpen');
6348 }
6349 });
6350
23192d23
AE
6351 this._main.click(function() { $searchBar.removeClass('searchBarOpen'); });
6352 },
6353
6354 /**
6355 * Initializes the button group lists, converting them into native dropdowns.
6356 */
6357 _initButtonGroupNavigation: function() {
6358 $('.buttonGroupNavigation:not(.jsMobileButtonGroupNavigation)').each(function(index, navigation) {
41385e13 6359 var $navigation = $(navigation).addClass('jsMobileButtonGroupNavigation');
23192d23
AE
6360 var $button = $('<a class="dropdownLabel"><span class="icon icon24 icon-list" /></a>').prependTo($navigation);
6361
f3e114a1 6362 $button.click(function() { $button.next().toggleClass('open'); return false; });
23192d23 6363 });
f3e114a1
AE
6364 },
6365
6366 /**
6367 * Closes menus.
6368 */
6369 _closeMenus: function() {
6370 $('.jsMobileButtonGroupNavigation > ul.open').removeClass('open');
23192d23
AE
6371 }
6372};
6373
5568e009
AE
6374/**
6375 * System notification overlays.
6376 *
6377 * @param string message
6378 * @param string cssClassNames
6379 */
6380WCF.System.Notification = Class.extend({
6381 /**
6382 * callback on notification close
6383 * @var object
6384 */
6385 _callback: null,
6386
d2e126c6
AE
6387 /**
6388 * CSS class names
6389 * @var string
6390 */
6391 _cssClassNames: '',
6392
6393 /**
6394 * notification message
6395 * @var string
6396 */
6397 _message: '',
6398
5568e009
AE
6399 /**
6400 * notification overlay
6401 * @var jQuery
6402 */
6403 _overlay: null,
6404
6405 /**
6406 * Creates a new system notification overlay.
6407 *
6408 * @param string message
6409 * @param string cssClassNames
6410 */
6411 init: function(message, cssClassNames) {
d2e126c6 6412 this._cssClassNames = cssClassNames || 'success';
11cf19be 6413 this._message = message || WCF.Language.get('wcf.global.success');
7c1fa02a 6414 this._overlay = $('#systemNotification');
5568e009 6415
d2e126c6 6416 if (!this._overlay.length) {
7ba6d79a 6417 this._overlay = $('<div id="systemNotification"><p></p></div>').hide().appendTo(document.body);
5568e009
AE
6418 }
6419 },
6420
6421 /**
6422 * Shows the notification overlay.
6423 *
6424 * @param object callback
6425 * @param integer duration
7c1fa02a
AE
6426 * @param string message
6427 * @param string cssClassName
5568e009 6428 */
7c1fa02a 6429 show: function(callback, duration, message, cssClassNames) {
5568e009
AE
6430 duration = parseInt(duration);
6431 if (!duration) duration = 2000;
6432
6433 if (callback && $.isFunction(callback)) {
6434 this._callback = callback;
6435 }
6436
d2e126c6
AE
6437 this._overlay.children('p').html((message || this._message));
6438 this._overlay.children('p').removeClass().addClass((cssClassNames || this._cssClassNames));
7c1fa02a 6439
5568e009
AE
6440 // hide overlay after specified duration
6441 new WCF.PeriodicalExecuter($.proxy(this._hide, this), duration);
6442
7ba6d79a 6443 this._overlay.wcfFadeIn(undefined, 300);
5568e009
AE
6444 },
6445
6446 /**
6447 * Hides the notification overlay after executing the callback.
6448 *
6449 * @param WCF.PeriodicalExecuter pe
6450 */
6451 _hide: function(pe) {
6452 if (this._callback !== null) {
6453 this._callback();
6454 }
6455
7ba6d79a 6456 this._overlay.wcfFadeOut(undefined, 300);
5568e009
AE
6457
6458 pe.stop();
6459 }
6460});
6461
34e158d9
AE
6462/**
6463 * Provides dialog-based confirmations.
6464 */
6465WCF.System.Confirmation = {
6466 /**
6467 * notification callback
6468 * @var object
6469 */
6470 _callback: null,
6471
6472 /**
6473 * confirmation dialog
6474 * @var jQuery
6475 */
6476 _dialog: null,
6477
f02b3f75
AE
6478 /**
6479 * callback parameters
6480 * @var object
6481 */
6482 _parameters: null,
6483
34e158d9
AE
6484 /**
6485 * dialog visibility
6486 * @var boolean
6487 */
6488 _visible: false,
6489
d36a62dc
MK
6490 /**
6491 * confirmation button
6492 * @var jQuery
6493 */
6494 _confirmationButton: null,
6495
34e158d9
AE
6496 /**
6497 * Displays a confirmation dialog.
6498 *
6499 * @param string message
6500 * @param object callback
f02b3f75 6501 * @param object parameters
73b84427 6502 * @param jQuery template
34e158d9 6503 */
73b84427 6504 show: function(message, callback, parameters, template) {
34e158d9
AE
6505 if (this._visible) {
6506 console.debug('[WCF.System.Confirmation] Confirmation dialog is already open, refusing action.');
6507 return;
6508 }
6509
6510 if (!$.isFunction(callback)) {
6511 console.debug('[WCF.System.Confirmation] Given callback is invalid, aborting.');
6512 return;
6513 }
6514
6515 this._callback = callback;
f02b3f75 6516 this._parameters = parameters;
e730016d
AE
6517
6518 var $render = true;
34e158d9
AE
6519 if (this._dialog === null) {
6520 this._createDialog();
e730016d 6521 $render = false;
34e158d9
AE
6522 }
6523
73b84427
AE
6524 this._dialog.find('#wcfSystemConfirmationContent').empty().hide();
6525 if (template && template.length) {
6526 template.appendTo(this._dialog.find('#wcfSystemConfirmationContent').show());
6527 }
6528
7b0937e3 6529 this._dialog.find('p').text(message);
34e158d9
AE
6530 this._dialog.wcfDialog({
6531 onClose: $.proxy(this._close, this),
6532 onShow: $.proxy(this._show, this),
6533 title: WCF.Language.get('wcf.global.confirmation.title')
6534 });
e730016d
AE
6535 if ($render) {
6536 this._dialog.wcfDialog('render');
6537 }
34e158d9 6538
d36a62dc 6539 this._confirmationButton.focus();
34e158d9
AE
6540 this._visible = true;
6541 },
6542
6543 /**
6544 * Creates the confirmation dialog on first use.
6545 */
6546 _createDialog: function() {
73b84427 6547 this._dialog = $('<div id="wcfSystemConfirmation" class="systemConfirmation"><p /><div id="wcfSystemConfirmationContent" /></div>').hide().appendTo(document.body);
582a1874 6548 var $formButtons = $('<div class="formSubmit" />').appendTo(this._dialog);
34e158d9 6549
d36a62dc 6550 this._confirmationButton = $('<button class="buttonPrimary">' + WCF.Language.get('wcf.global.confirmation.confirm') + '</button>').data('action', 'confirm').click($.proxy(this._click, this)).appendTo($formButtons);
34e158d9
AE
6551 $('<button>' + WCF.Language.get('wcf.global.confirmation.cancel') + '</button>').data('action', 'cancel').click($.proxy(this._click, this)).appendTo($formButtons);
6552 },
6553
6554 /**
6555 * Handles button clicks.
6556 *
6557 * @param object event
6558 */
6559 _click: function(event) {
6560 this._notify($(event.currentTarget).data('action'));
6561 },
6562
6563 /**
6564 * Handles dialog being closed.
6565 */
6566 _close: function() {
6567 if (this._visible) {
6568 this._notify('cancel');
6569 }
6570 },
6571
6572 /**
6573 * Notifies callback upon user's decision.
6574 *
6575 * @param string action
6576 */
6577 _notify: function(action) {
6578 this._visible = false;
6579 this._dialog.wcfDialog('close');
6580
f02b3f75 6581 this._callback(action, this._parameters);
34e158d9
AE
6582 },
6583
6584 /**
6585 * Tries to set focus on confirm button.
6586 */
6587 _show: function() {
582a1874 6588 this._dialog.find('button.buttonPrimary').blur().focus();
34e158d9
AE
6589 }
6590};
6591
b2918c9d
TD
6592/**
6593 * Disables the ability to scroll the page.
6594 */
6595WCF.System.DisableScrolling = {
6596 /**
6597 * number of times scrolling was disabled (nested calls)
6598 * @var integer
6599 */
6600 _depth: 0,
6601
6602 /**
6603 * old overflow-value of the body element
6604 * @var string
6605 */
6606 _oldOverflow: null,
6607
6608 /**
6609 * Disables scrolling.
6610 */
6611 disable: function () {
1dd0c00d
AE
6612 // do not block scrolling on touch devices
6613 if ($.browser.touch) {
6614 return;
6615 }
6616
b2918c9d
TD
6617 if (this._depth === 0) {
6618 this._oldOverflow = $(document.body).css('overflow');
6619 $(document.body).css('overflow', 'hidden');
6620 }
6621
6622 this._depth++;
6623 },
6624
6625 /**
6626 * Enables scrolling again.
6627 * Must be called the same number of times disable() was called to enable scrolling.
6628 */
6629 enable: function () {
6630 if (this._depth === 0) return;
6631
6632 this._depth--;
6633
6634 if (this._depth === 0) {
6635 $(document.body).css('overflow', this._oldOverflow);
6636 }
6637 }
6638};
6639
d83e246c
AE
6640/**
6641 * Provides the 'jump to page' overlay.
6642 */
6643WCF.System.PageNavigation = {
6644 /**
6645 * submit button
6646 * @var jQuery
6647 */
6648 _button: null,
6649
6650 /**
6651 * page No description
6652 * @var jQuery
6653 */
6654 _description: null,
6655
6656 /**
6657 * dialog overlay
6658 * @var jQuery
6659 */
6660 _dialog: null,
6661
6662 /**
6663 * active element id
6664 * @var string
6665 */
6666 _elementID: '',
6667
6668 /**
6669 * list of tracked navigation bars
6670 * @var object
6671 */
6672 _elements: { },
6673
6674 /**
6675 * page No input
6676 * @var jQuery
6677 */
6678 _pageNo: null,
6679
6680 /**
6681 * Initializes the 'jump to page' overlay for given selector.
6682 *
6683 * @param string selector
3bdc6920 6684 * @param object callback
d83e246c 6685 */
3bdc6920 6686 init: function(selector, callback) {
d83e246c
AE
6687 var $elements = $(selector);
6688 if (!$elements.length) {
6689 return;
6690 }
6691
3bdc6920 6692 callback = callback || null;
3bdc6920
AE
6693 if (callback !== null && !$.isFunction(callback)) {
6694 console.debug("[WCF.System.PageNavigation] Callback for selector '" + selector + "' is invalid, aborting.");
6695 return;
6696 }
6697
6698 this._initElements($elements, callback);
d83e246c
AE
6699 },
6700
6701 /**
6702 * Initializes the 'jump to page' overlay for given elements.
6703 *
6704 * @param jQuery elements
3bdc6920 6705 * @param object callback
d83e246c 6706 */
3bdc6920 6707 _initElements: function(elements, callback) {
d83e246c
AE
6708 var self = this;
6709 elements.each(function(index, element) {
6710 var $element = $(element);
6711 var $elementID = $element.wcfIdentify();
25c2c382 6712
d83e246c
AE
6713 if (self._elements[$elementID] === undefined) {
6714 self._elements[$elementID] = $element;
6715 $element.find('li.jumpTo').data('elementID', $elementID).click($.proxy(self._click, self));
6716 }
3bdc6920 6717 }).data('callback', callback);
d83e246c
AE
6718 },
6719
6720 /**
6721 * Shows the 'jump to page' overlay.
6722 *
6723 * @param object event
6724 */
6725 _click: function(event) {
6726 this._elementID = $(event.currentTarget).data('elementID');
6727
6728 if (this._dialog === null) {
6729 this._dialog = $('<div id="pageNavigationOverlay" />').hide().appendTo(document.body);
6730
6731 var $fieldset = $('<fieldset><legend>' + WCF.Language.get('wcf.global.page.jumpTo') + '</legend></fieldset>').appendTo(this._dialog);
6732 $('<dl><dt><label for="jsPageNavigationPageNo">' + WCF.Language.get('wcf.global.page.jumpTo') + '</label></dt><dd></dd></dl>').appendTo($fieldset);
adf4d051 6733 this._pageNo = $('<input type="number" id="jsPageNavigationPageNo" value="1" min="1" max="1" class="tiny" />').keyup($.proxy(this._keyUp, this)).appendTo($fieldset.find('dd'));
d83e246c
AE
6734 this._description = $('<small></small>').insertAfter(this._pageNo);
6735 var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog);
6736 this._button = $('<button class="buttonPrimary">' + WCF.Language.get('wcf.global.button.submit') + '</button>').click($.proxy(this._submit, this)).appendTo($formSubmit);
6737 }
6738
6739 this._button.enable();
6740 this._description.html(WCF.Language.get('wcf.global.page.jumpTo.description').replace(/#pages#/, this._elements[this._elementID].data('pages')));
adf4d051 6741 this._pageNo.val(this._elements[this._elementID].data('pages')).attr('max', this._elements[this._elementID].data('pages'));
d83e246c
AE
6742
6743 this._dialog.wcfDialog({
6744 'title': WCF.Language.get('wcf.global.page.pageNavigation')
6745 });
6746 },
6747
6748 /**
6749 * Validates the page No input.
6750 */
6751 _keyUp: function() {
6752 var $pageNo = parseInt(this._pageNo.val()) || 0;
6753 if ($pageNo < 1 || $pageNo > this._pageNo.attr('max')) {
6754 this._button.disable();
6755 }
6756 else {
6757 this._button.enable();
6758 }
6759 },
6760
6761 /**
6762 * Redirects to given page No.
6763 */
6764 _submit: function() {
3bdc6920
AE
6765 var $pageNavigation = this._elements[this._elementID];
6766 if ($pageNavigation.data('callback') === null) {
6767 var $redirectURL = $pageNavigation.data('link').replace(/pageNo=%d/, 'pageNo=' + this._pageNo.val());
6768 window.location = $redirectURL;
6769 }
6770 else {
6771 $pageNavigation.data('callback')(this._pageNo.val());
a19475d3 6772 this._dialog.wcfDialog('close');
3bdc6920 6773 }
d83e246c
AE
6774 }
6775};
6776
dd932bc6
AE
6777/**
6778 * Sends periodical requests to protect the session from expiring. By default
6779 * it will send a request 1 minute before it would expire.
6780 *
6781 * @param integer seconds
6782 */
6783WCF.System.KeepAlive = Class.extend({
6784 /**
6785 * Initializes the WCF.System.KeepAlive class.
6786 *
6787 * @param integer seconds
6788 */
6789 init: function(seconds) {
9b1326f4 6790 new WCF.PeriodicalExecuter(function(pe) {
dd932bc6
AE
6791 new WCF.Action.Proxy({
6792 autoSend: true,
6793 data: {
6794 actionName: 'keepAlive',
6795 className: 'wcf\\data\\session\\SessionAction'
90b713ac 6796 },
9b1326f4 6797 failure: function() { pe.stop(); },
88432fe0
AE
6798 showLoadingOverlay: false,
6799 suppressErrors: true
dd932bc6
AE
6800 });
6801 }, (seconds * 1000));
6802 }
6803});
6804
e7f87db1
AE
6805/**
6806 * Default implementation for inline editors.
be8e935b
AE
6807 *
6808 * @param string elementSelector
e7f87db1
AE
6809 */
6810WCF.InlineEditor = Class.extend({
6811 /**
6812 * list of registered callbacks
6813 * @var array<object>
6814 */
6815 _callbacks: [ ],
6816
7c1fa02a
AE
6817 /**
6818 * list of dropdown selections
6819 * @var object
6820 */
6821 _dropdowns: { },
6822
e7f87db1
AE
6823 /**
6824 * list of container elements
6825 * @var object
6826 */
6827 _elements: { },
6828
6829 /**
7c1fa02a
AE
6830 * notification object
6831 * @var WCF.System.Notification
e7f87db1 6832 */
7c1fa02a 6833 _notification: null,
e7f87db1 6834
e7f87db1
AE
6835 /**
6836 * list of known options
6837 * @var array<object>
6838 */
6839 _options: [ ],
6840
6841 /**
6842 * action proxy
6843 * @var WCF.Action.Proxy
6844 */
6845 _proxy: null,
6846
f3ca0ddf
AE
6847 /**
6848 * list of data to update upon success
6849 * @var array<object>
6850 */
6851 _updateData: [ ],
6852
e7f87db1
AE
6853 /**
6854 * Initializes a new inline editor.
6855 */
f3ca0ddf
AE
6856 init: function(elementSelector) {
6857 var $elements = $(elementSelector);
e7f87db1
AE
6858 if (!$elements.length) {
6859 return;
6860 }
6861
150fb135
AE
6862 this._setOptions();
6863 var $quickOption = '';
6864 for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
6865 if (this._options[$i].isQuickOption) {
6866 $quickOption = this._options[$i].optionName;
6867 break;
6868 }
6869 }
6870
e7f87db1
AE
6871 var self = this;
6872 $elements.each(function(index, element) {
6873 var $element = $(element);
6874 var $elementID = $element.wcfIdentify();
6875
6876 // find trigger element
f3ca0ddf 6877 var $trigger = self._getTriggerElement($element);
e7f87db1
AE
6878 if ($trigger === null || $trigger.length !== 1) {
6879 return;
6880 }
6881
f3ca0ddf 6882 $trigger.click($.proxy(self._show, self)).data('elementID', $elementID);
150fb135
AE
6883 if ($quickOption) {
6884 // simulate click on target action
6885 $trigger.disableSelection().data('optionName', $quickOption).dblclick($.proxy(self._click, self));
6886 }
e7f87db1
AE
6887
6888 // store reference
6889 self._elements[$elementID] = $element;
6890 });
6891
6892 this._proxy = new WCF.Action.Proxy({
f3ca0ddf 6893 success: $.proxy(this._success, this)
e7f87db1 6894 });
f3ca0ddf 6895
83428b7f 6896 WCF.CloseOverlayHandler.addCallback('WCF.InlineEditor', $.proxy(this._closeAll, this));
7c1fa02a
AE
6897
6898 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success');
83428b7f
AE
6899 },
6900
6901 /**
6902 * Closes all inline editors.
6903 */
6904 _closeAll: function() {
6905 for (var $elementID in this._elements) {
6906 this._hide($elementID);
6907 }
e7f87db1
AE
6908 },
6909
f3ca0ddf
AE
6910 /**
6911 * Sets options for this inline editor.
6912 */
6913 _setOptions: function() {
6914 this._options = [ ];
be8e935b 6915 },
f3ca0ddf 6916
e7f87db1
AE
6917 /**
6918 * Register an option callback for validation and execution.
6919 *
6920 * @param object callback
6921 */
6922 registerCallback: function(callback) {
6923 if ($.isFunction(callback)) {
6924 this._callbacks.push(callback);
6925 }
6926 },
6927
6928 /**
6929 * Returns the triggering element.
6930 *
6931 * @param jQuery element
6932 * @return jQuery
6933 */
6934 _getTriggerElement: function(element) {
6935 return null;
6936 },
6937
6938 /**
6939 * Shows a dropdown menu if options are available.
6940 *
6941 * @param object event
6942 */
6943 _show: function(event) {
6944 var $elementID = $(event.currentTarget).data('elementID');
f24f0823 6945
3ef8dee9
AE
6946 // build dropdown
6947 var $trigger = null;
e7f87db1 6948 if (!this._dropdowns[$elementID]) {
3ef8dee9
AE
6949 $trigger = this._getTriggerElement(this._elements[$elementID]).addClass('dropdownToggle').wrap('<span class="dropdown" />');
6950 this._dropdowns[$elementID] = $('<ul class="dropdownMenu" />').insertAfter($trigger);
e7f87db1 6951 }
84abb173 6952 this._dropdowns[$elementID].empty();
e7f87db1
AE
6953
6954 // validate options
6955 var $hasOptions = false;
b3524514 6956 var $lastElementType = '';
e7f87db1
AE
6957 for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
6958 var $option = this._options[$i];
6959
7c1fa02a 6960 if ($option.optionName === 'divider') {
b3524514
AE
6961 if ($lastElementType !== '' && $lastElementType !== 'divider') {
6962 $('<li class="dropdownDivider" />').appendTo(this._dropdowns[$elementID]);
6963 $lastElementType = $option.optionName;
6964 }
7c1fa02a
AE
6965 }
6966 else if (this._validate($elementID, $option.optionName) || this._validateCallbacks($elementID, $option.optionName)) {
e7f87db1 6967 var $listItem = $('<li><span>' + $option.label + '</span></li>').appendTo(this._dropdowns[$elementID]);
0dcde1e2 6968 $listItem.data('elementID', $elementID).data('optionName', $option.optionName).data('isQuickOption', ($option.isQuickOption ? true : false)).click($.proxy(this._click, this));
e7f87db1
AE
6969
6970 $hasOptions = true;
b3524514 6971 $lastElementType = $option.optionName;
e7f87db1
AE
6972 }
6973 }
6974
6975 if ($hasOptions) {
4c5cfd1a
MS
6976 // if last child is divider, remove it
6977 var $lastChild = this._dropdowns[$elementID].children().last();
6978 if ($lastChild.hasClass('dropdownDivider')) {
6979 $lastChild.remove();
6980 }
0dcde1e2
AE
6981
6982 // check if only element is a quick option
6983 var $quickOption = null;
6984 var $count = 0;
6985 this._dropdowns[$elementID].children().each(function(index, child) {
6986 var $child = $(child);
6987 if (!$child.hasClass('dropdownDivider')) {
6988 if ($child.data('isQuickOption')) {
6989 $quickOption = $child;
6990 }
6991 else {
6992 $count++;
6993 }
6994 }
6995 });
60dbffb8 6996
0dcde1e2
AE
6997 if (!$count) {
6998 $quickOption.trigger('click');
6999
7000 if ($trigger === null) {
7001 WCF.Dropdown.close($trigger.parents('.dropdown').wcfIdentify());
7002 }
7003
7004 return false;
7005 }
e7f87db1 7006 }
83428b7f 7007
3ef8dee9
AE
7008 if ($trigger !== null) {
7009 WCF.Dropdown.initDropdown($trigger, true);
7010 }
7011
83428b7f 7012 return false;
e7f87db1
AE
7013 },
7014
7015 /**
7016 * Validates an option.
7017 *
7018 * @param string elementID
7019 * @param string optionName
7020 * @returns boolean
7021 */
7022 _validate: function(elementID, optionName) {
7023 return false;
7024 },
7025
7026 /**
7027 * Validates an option provided by callbacks.
7028 *
7029 * @param string elementID
7030 * @param string optionName
7031 * @return boolean
7032 */
7033 _validateCallbacks: function(elementID, optionName) {
7034 var $length = this._callbacks.length;
7035 if ($length) {
7036 for (var $i = 0; $i < $length; $i++) {
7037 if (this._callbacks[$i].validate(this._elements[elementID], optionName)) {
7038 return true;
7039 }
7040 }
7041 }
7042
7043 return false;
7044 },
7045
7046 /**
7047 * Handles AJAX responses.
7048 *
7049 * @param object data
7050 * @param string textStatus
7051 * @param jQuery jqXHR
7052 */
f3ca0ddf
AE
7053 _success: function(data, textStatus, jqXHR) {
7054 var $length = this._updateData.length;
7055 if (!$length) {
7056 return;
7057 }
7058
8eace9e0 7059 this._updateState(data);
f3ca0ddf
AE
7060
7061 this._updateData = [ ];
7062 },
7063
7064 /**
7065 * Update element states based upon update data.
8eace9e0
AE
7066 *
7067 * @param object data
f3ca0ddf 7068 */
8eace9e0 7069 _updateState: function(data) { },
e7f87db1
AE
7070
7071 /**
7072 * Handles clicks within dropdown.
7073 *
7074 * @param object event
7075 */
7076 _click: function(event) {
7077 var $listItem = $(event.currentTarget);
7078 var $elementID = $listItem.data('elementID');
7079 var $optionName = $listItem.data('optionName');
7080
7081 if (!this._execute($elementID, $optionName)) {
7082 this._executeCallback($elementID, $optionName);
7083 }
7084
7085 this._hide($elementID);
7086 },
7087
7088 /**
7089 * Executes actions associated with an option.
7090 *
7091 * @param string elementID
7092 * @param string optionName
7093 * @return boolean
7094 */
7095 _execute: function(elementID, optionName) {
7096 return false;
7097 },
7098
7099 /**
7100 * Executes actions associated with an option provided by callbacks.
7101 *
7102 * @param string elementID
7103 * @param string optionName
7104 * @return boolean
7105 */
7106 _executeCallback: function(elementID, optionName) {
7107 var $length = this._callbacks.length;
7108 if ($length) {
7109 for (var $i = 0; $i < $length; $i++) {
7110 if (this._callbacks[$i].execute(this._elements[elementID], optionName)) {
7111 return true;
7112 }
7113 }
7114 }
7115
7116 return false;
7117 },
7118
7119 /**
7120 * Hides a dropdown menu.
7121 *
7122 * @param string elementID
7123 */
7124 _hide: function(elementID) {
83428b7f 7125 if (this._dropdowns[elementID]) {
21b08838 7126 this._dropdowns[elementID].empty().removeClass('dropdownOpen');
83428b7f 7127 }
e7f87db1
AE
7128 }
7129});
7130
c1aaf7a7
MW
7131/**
7132 * Default implementation for ajax file uploads
7133 *
7134 * @param jquery buttonSelector
7135 * @param jquery fileListSelector
7136 * @param string className
7137 * @param jquery options
7138 */
7139WCF.Upload = Class.extend({
7140 /**
7141 * name of the upload field
9f959ced 7142 * @var string
c1aaf7a7
MW
7143 */
7144 _name: '__files[]',
7145
7146 /**
a2f01563 7147 * button selector
9f959ced 7148 * @var jQuery
c1aaf7a7
MW
7149 */
7150 _buttonSelector: null,
7151
7152 /**
a2f01563 7153 * file list selector
9f959ced 7154 * @var jQuery
c1aaf7a7
MW
7155 */
7156 _fileListSelector: null,
7157
7158 /**
7159 * upload file
9f959ced 7160 * @var jQuery
c1aaf7a7
MW
7161 */
7162 _fileUpload: null,
7163
7164 /**
7165 * class name
9f959ced 7166 * @var string
c1aaf7a7
MW
7167 */
7168 _className: '',
7169
e919d1d3
AE
7170 /**
7171 * iframe for IE<10 fallback
7172 * @var jQuery
7173 */
7174 _iframe: null,
7175
d9e09941
AE
7176 /**
7177 * internal file id
7178 * @var integer
7179 */
7180 _internalFileID: 0,
7181
c1aaf7a7
MW
7182 /**
7183 * additional options
9f959ced 7184 * @var jQuery
c1aaf7a7
MW
7185 */
7186 _options: {},
7187
7188 /**
7189 * upload matrix
9f959ced 7190 * @var array
c1aaf7a7
MW
7191 */
7192 _uploadMatrix: [],
7193
7194 /**
9f959ced
MS
7195 * true, if the active user's browser supports ajax file uploads
7196 * @var boolean
c1aaf7a7
MW
7197 */
7198 _supportsAJAXUpload: true,
7199
7200 /**
7201 * fallback overlay for stupid browsers
9f959ced 7202 * @var jquery
c1aaf7a7
MW
7203 */
7204 _overlay: null,
7205
7206 /**
7207 * Initializes a new upload handler.
e919d1d3 7208 *
a2f01563
AE
7209 * @param string buttonSelector
7210 * @param string fileListSelector
e919d1d3
AE
7211 * @param string className
7212 * @param object options
c1aaf7a7
MW
7213 */
7214 init: function(buttonSelector, fileListSelector, className, options) {
7215 this._buttonSelector = buttonSelector;
7216 this._fileListSelector = fileListSelector;
7217 this._className = className;
d9e09941 7218 this._internalFileID = 0;
c1aaf7a7
MW
7219 this._options = $.extend(true, {
7220 action: 'upload',
7221 multiple: false,
7222 url: 'index.php/AJAXUpload/?t=' + SECURITY_TOKEN + SID_ARG_2ND
e919d1d3 7223 }, options || { });
c1aaf7a7
MW
7224
7225 // check for ajax upload support
7226 var $xhr = new XMLHttpRequest();
7227 this._supportsAJAXUpload = ($xhr && ('upload' in $xhr) && ('onprogress' in $xhr.upload));
7228
7229 // create upload button
7230 this._createButton();
7231 },
7232
7233 /**
7234 * Creates the upload button.
7235 */
7236 _createButton: function() {
7237 if (this._supportsAJAXUpload) {
0d2b7a3b 7238 this._fileUpload = $('<input type="file" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/>');
c1aaf7a7 7239 this._fileUpload.change($.proxy(this._upload, this));
0d2b7a3b 7240 var $button = $('<p class="button uploadButton"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>');
5427002e 7241 $button.prepend(this._fileUpload);
c1aaf7a7
MW
7242 }
7243 else {
0d2b7a3b 7244 var $button = $('<p class="button uploadFallbackButton"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>');
c1aaf7a7
MW
7245 $button.click($.proxy(this._showOverlay, this));
7246 }
7247
7248 this._insertButton($button);
7249 },
7250
7251 /**
7252 * Inserts the upload button.
e919d1d3
AE
7253 *
7254 * @param jQuery button
c1aaf7a7
MW
7255 */
7256 _insertButton: function(button) {
7257 this._buttonSelector.append(button);
7258 },
7259
0d2b7a3b
MS
7260 /**
7261 * Removes the upload button.
7262 */
7263 _removeButton: function() {
7264 var $selector = '.uploadButton';
7265 if (!this._supportsAJAXUpload) {
7266 $selector = '.uploadFallbackButton';
7267 }
7268
7269 this._buttonSelector.find($selector).remove();
7270 },
7271
c1aaf7a7
MW
7272 /**
7273 * Callback for file uploads.
7274 */
7275 _upload: function() {
7276 var $files = this._fileUpload.prop('files');
e919d1d3 7277 if ($files.length) {
c1aaf7a7 7278 var $fd = new FormData();
e919d1d3 7279 var $uploadID = this._createUploadMatrix($files);
c1aaf7a7 7280
404c9f0d
AE
7281 // no more files left, abort
7282 if (!this._uploadMatrix[$uploadID].length) {
7283 return;
7284 }
7285
e919d1d3 7286 for (var $i = 0, $length = $files.length; $i < $length; $i++) {
d9e09941
AE
7287 if (this._uploadMatrix[$uploadID][$i]) {
7288 var $internalFileID = this._uploadMatrix[$uploadID][$i].data('internalFileID');
7289 $fd.append('__files[' + $internalFileID + ']', $files[$i]);
404c9f0d 7290 }
c1aaf7a7 7291 }
e919d1d3 7292
c1aaf7a7
MW
7293 $fd.append('actionName', this._options.action);
7294 $fd.append('className', this._className);
32f6cd95
MW
7295 var $additionalParameters = this._getParameters();
7296 for (var $name in $additionalParameters) {
d9e09941 7297 $fd.append('parameters[' + $name + ']', $additionalParameters[$name]);
32f6cd95 7298 }
c1aaf7a7 7299
e919d1d3 7300 var self = this;
c1aaf7a7
MW
7301 $.ajax({
7302 type: 'POST',
7303 url: this._options.url,
7304 enctype: 'multipart/form-data',
7305 data: $fd,
7306 contentType: false,
7307 processData: false,
32f6cd95
MW
7308 success: function(data, textStatus, jqXHR) {
7309 self._success($uploadID, data);
7310 },
c1aaf7a7
MW
7311 error: $.proxy(this._error, this),
7312 xhr: function() {
7313 var $xhr = $.ajaxSettings.xhr();
7314 if ($xhr) {
7315 $xhr.upload.addEventListener('progress', function(event) {
32f6cd95 7316 self._progress($uploadID, event);
c1aaf7a7
MW
7317 }, false);
7318 }
7319 return $xhr;
7320 }
7321 });
7322 }
7323 },
7324
7325 /**
e919d1d3
AE
7326 * Creates upload matrix for provided files.
7327 *
7328 * @param array<object> files
7329 * @return integer
c1aaf7a7 7330 */
e919d1d3
AE
7331 _createUploadMatrix: function(files) {
7332 if (files.length) {
7333 var $uploadID = this._uploadMatrix.length;
7334 this._uploadMatrix[$uploadID] = [ ];
7335
7336 for (var $i = 0, $length = files.length; $i < $length; $i++) {
7337 var $file = files[$i];
7338 var $li = this._initFile($file);
7339
404c9f0d 7340 if (!$li.hasClass('uploadFailed')) {
d9e09941
AE
7341 $li.data('filename', $file.name).data('internalFileID', this._internalFileID++);
7342 this._uploadMatrix[$uploadID][$i] = $li;
404c9f0d 7343 }
e919d1d3
AE
7344 }
7345
7346 return $uploadID;
7347 }
7348
7349 return null;
c1aaf7a7
MW
7350 },
7351
7352 /**
e919d1d3
AE
7353 * Callback for success event.
7354 *
7355 * @param integer uploadID
7356 * @param object data
c1aaf7a7 7357 */
e919d1d3 7358 _success: function(uploadID, data) { },
c1aaf7a7
MW
7359
7360 /**
e919d1d3
AE
7361 * Callback for error event.
7362 *
7363 * @param jQuery jqXHR
7364 * @param string textStatus
7365 * @param string errorThrown
7366 */
7367 _error: function(jqXHR, textStatus, errorThrown) { },
7368
7369 /**
7370 * Callback for progress event.
7371 *
7372 * @param integer uploadID
7373 * @param object event
c1aaf7a7 7374 */
32f6cd95 7375 _progress: function(uploadID, event) {
c1aaf7a7
MW
7376 var $percentComplete = Math.round(event.loaded * 100 / event.total);
7377
d9e09941 7378 for (var $i in this._uploadMatrix[uploadID]) {
c1aaf7a7
MW
7379 this._uploadMatrix[uploadID][$i].find('progress').attr('value', $percentComplete);
7380 }
7381 },
7382
32f6cd95
MW
7383 /**
7384 * Returns additional parameters.
e919d1d3
AE
7385 *
7386 * @return object
32f6cd95
MW
7387 */
7388 _getParameters: function() {
7389 return {};
7390 },
c1aaf7a7 7391
e919d1d3
AE
7392 /**
7393 * Initializes list item for uploaded file.
7394 *
7395 * @return jQuery
7396 */
c1aaf7a7 7397 _initFile: function(file) {
e919d1d3 7398 return $('<li>' + file.name + ' (' + file.size + ')<progress max="100" /></li>').appendTo(this._fileListSelector);
c1aaf7a7
MW
7399 },
7400
7401 /**
7402 * Shows the fallback overlay (work in progress)
7403 */
7404 _showOverlay: function() {
c1aaf7a7 7405 // create iframe
e919d1d3
AE
7406 if (this._iframe === null) {
7407 this._iframe = $('<iframe name="__fileUploadIFrame" />').hide().appendTo(document.body);
7408 }
c1aaf7a7 7409
e919d1d3
AE
7410 // create overlay
7411 if (!this._overlay) {
7412 this._overlay = $('<div><form enctype="multipart/form-data" method="post" action="' + this._options.url + '" target="__fileUploadIFrame" /></div>').hide().appendTo(document.body);
7413
7414 var $form = this._overlay.find('form');
5427002e 7415 $('<dl class="wide"><dd><input type="file" id="__fileUpload" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/></dd></dl>').appendTo($form);
e919d1d3
AE
7416 $('<div class="formSubmit"><input type="submit" value="Upload" accesskey="s" /></div></form>').appendTo($form);
7417
7418 $('<input type="hidden" name="isFallback" value="1" />').appendTo($form);
7419 $('<input type="hidden" name="actionName" value="' + this._options.action + '" />').appendTo($form);
7420 $('<input type="hidden" name="className" value="' + this._className + '" />').appendTo($form);
7421 var $additionalParameters = this._getParameters();
7422 for (var $name in $additionalParameters) {
7423 $('<input type="hidden" name="' + $name + '" value="' + $additionalParameters[$name] + '" />').appendTo($form);
7424 }
7425
7426 $form.submit($.proxy(function() {
7427 var $file = {
7428 name: this._getFilename(),
5427002e 7429 size: ''
e919d1d3
AE
7430 };
7431
7432 var $uploadID = this._createUploadMatrix([ $file ]);
7433 var self = this;
7434 this._iframe.data('loading', true).off('load').load(function() { self._evaluateResponse($uploadID); });
7435 this._overlay.wcfDialog('close');
7436 }, this));
7437 }
c1aaf7a7
MW
7438
7439 this._overlay.wcfDialog({
5427002e 7440 title: WCF.Language.get('wcf.global.button.upload')
c1aaf7a7 7441 });
e919d1d3
AE
7442 },
7443
7444 /**
7445 * Evaluates iframe response.
7446 *
7447 * @param integer uploadID
7448 */
7449 _evaluateResponse: function(uploadID) {
7450 var $returnValues = $.parseJSON(this._iframe.contents().find('pre').html());
7451 this._success(uploadID, $returnValues);
7452 },
7453
7454 /**
7455 * Returns name of selected file.
7456 *
7457 * @return string
7458 */
7459 _getFilename: function() {
7460 return $('#__fileUpload').val().split('\\').pop();
c1aaf7a7
MW
7461 }
7462});
7463
dedc6a49
MS
7464/**
7465 * Default implementation for parallel AJAX file uploads.
7466 */
7467WCF.Upload.Parallel = WCF.Upload.extend({
7468 /**
7469 * @see WCF.Upload.init()
7470 */
7471 init: function(buttonSelector, fileListSelector, className, options) {
7472 // force multiple uploads
5eaa16b7
MS
7473 options = $.extend(true, options || { }, {
7474 multiple: true
7475 });
dedc6a49
MS
7476
7477 this._super(buttonSelector, fileListSelector, className, options);
7478 },
7479
7480 /**
7481 * @see WCF.Upload._upload()
7482 */
7483 _upload: function() {
7484 var $files = this._fileUpload.prop('files');
7485 for (var $i = 0, $length = $files.length; $i < $length; $i++) {
7486 var $file = $files[$i];
7487 var $formData = new FormData();
7488 var $internalFileID = this._createUploadMatrix($file);
7489
7490 if (!this._uploadMatrix[$internalFileID].length) {
7491 continue;
7492 }
7493
7494 $formData.append('__files[' + $internalFileID + ']', $file);
7495 $formData.append('actionName', this._options.action);
7496 $formData.append('className', this._className);
7497 var $additionalParameters = this._getParameters();
7498 for (var $name in $additionalParameters) {
7499 $formData.append('parameters[' + $name + ']', $additionalParameters[$name]);
7500 }
7501
7502 this._sendRequest($internalFileID, $formData);
7503 }
7504 },
7505
7506 /**
7507 * Sends an AJAX request to upload a file.
7508 *
7509 * @param integer internalFileID
7510 * @param FormData formData
7511 */
7512 _sendRequest: function(internalFileID, formData) {
7513 var self = this;
7514 $.ajax({
7515 type: 'POST',
7516 url: this._options.url,
7517 enctype: 'multipart/form-data',
7518 data: formData,
7519 contentType: false,
7520 processData: false,
7521 success: function(data, textStatus, jqXHR) {
7522 self._success(internalFileID, data);
7523 },
7524 error: $.proxy(this._error, this),
7525 xhr: function() {
7526 var $xhr = $.ajaxSettings.xhr();
7527 if ($xhr) {
7528 $xhr.upload.addEventListener('progress', function(event) {
7529 self._progress(internalFileID, event);
7530 }, false);
7531 }
7532 return $xhr;
7533 }
7534 });
7535 },
7536
7537 /**
7538 * Creates upload matrix for provided file and returns its internal file id.
7539 *
7540 * @param object file
7541 * @return integer
7542 */
7543 _createUploadMatrix: function(file) {
7544 var $li = this._initFile(file);
7545 if (!$li.hasClass('uploadFailed')) {
7546 $li.data('filename', file.name).data('internalFileID', this._internalFileID);
7547 this._uploadMatrix[this._internalFileID++] = $li;
7548
7549 return this._internalFileID - 1;
7550 }
7551
7552 return null;
7553 },
7554
7555 /**
7556 * Callback for success event.
7557 *
7558 * @param integer internalFileID
7559 * @param object data
7560 */
7561 _success: function(internalFileID, data) { },
7562
7563 /**
7564 * Callback for progress event.
7565 *
7566 * @param integer internalFileID
7567 * @param object event
7568 */
7569 _progress: function(internalFileID, event) {
7570 var $percentComplete = Math.round(event.loaded * 100 / event.total);
7571
7572 this._uploadMatrix[internalFileID].find('progress').attr('value', $percentComplete);
7573 },
7574
7575 /**
7576 * @see WCF.Upload._showOverlay()
7577 */
7578 _showOverlay: function() {
7579 // create iframe
7580 if (this._iframe === null) {
7581 this._iframe = $('<iframe name="__fileUploadIFrame" />').hide().appendTo(document.body);
7582 }
7583
7584 // create overlay
7585 if (!this._overlay) {
7586 this._overlay = $('<div><form enctype="multipart/form-data" method="post" action="' + this._options.url + '" target="__fileUploadIFrame" /></div>').hide().appendTo(document.body);
7587
7588 var $form = this._overlay.find('form');
7589 $('<dl class="wide"><dd><input type="file" id="__fileUpload" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/></dd></dl>').appendTo($form);
7590 $('<div class="formSubmit"><input type="submit" value="Upload" accesskey="s" /></div></form>').appendTo($form);
7591
7592 $('<input type="hidden" name="isFallback" value="1" />').appendTo($form);
7593 $('<input type="hidden" name="actionName" value="' + this._options.action + '" />').appendTo($form);
7594 $('<input type="hidden" name="className" value="' + this._className + '" />').appendTo($form);
7595 var $additionalParameters = this._getParameters();
7596 for (var $name in $additionalParameters) {
7597 $('<input type="hidden" name="' + $name + '" value="' + $additionalParameters[$name] + '" />').appendTo($form);
7598 }
7599
7600 $form.submit($.proxy(function() {
7601 var $file = {
7602 name: this._getFilename(),
7603 size: ''
7604 };
7605
7606 var $internalFileID = this._createUploadMatrix($file);
7607 var self = this;
7608 this._iframe.data('loading', true).off('load').load(function() { self._evaluateResponse($internalFileID); });
7609 this._overlay.wcfDialog('close');
7610 }, this));
7611 }
7612
7613 this._overlay.wcfDialog({
7614 title: WCF.Language.get('wcf.global.button.upload')
7615 });
7616 },
7617
7618 /**
7619 * Evaluates iframe response.
7620 *
7621 * @param integer internalFileID
7622 */
7623 _evaluateResponse: function(internalFileID) {
7624 var $returnValues = $.parseJSON(this._iframe.contents().find('pre').html());
7625 this._success(internalFileID, $returnValues);
7626 }
7627});
7628
ee1a6ccb
AE
7629/**
7630 * Namespace for sortables.
7631 */
dedc6a49 7632WCF.Sortable = { };
ee1a6ccb
AE
7633
7634/**
7635 * Sortable implementation for lists.
7636 *
7637 * @param string containerID
7638 * @param string className
3926f3e3
AE
7639 * @param integer offset
7640 * @param object options
ee1a6ccb 7641 */
3926f3e3 7642WCF.Sortable.List = Class.extend({
2a16194a
AE
7643 /**
7644 * additional parameters for AJAX request
7645 * @var object
7646 */
7647 _additionalParameters: { },
7648
ee1a6ccb
AE
7649 /**
7650 * action class name
7651 * @var string
7652 */
7653 _className: '',
7654
7655 /**
7656 * container id
7657 * @var string
7658 */
7659 _containerID: '',
7660
7661 /**
7662 * container object
7663 * @var jQuery
7664 */
7665 _container: null,
7666
7667 /**
7668 * notification object
7669 * @var WCF.System.Notification
7670 */
7671 _notification: null,
7672
b007ce67
AE
7673 /**
7674 * show order offset
7675 * @var integer
7676 */
7677 _offset: 0,
7678
076e8a3f
AE
7679 /**
7680 * list of options
7681 * @var object
7682 */
7683 _options: { },
7684
ee1a6ccb
AE
7685 /**
7686 * proxy object
7687 * @var WCF.Action.Proxy
7688 */
7689 _proxy: null,
7690
7691 /**
7692 * object structure
7693 * @var object
7694 */
7695 _structure: { },
7696
7697 /**
7698 * Creates a new sortable list.
7699 *
7700 * @param string containerID
7701 * @param string className
b007ce67 7702 * @param integer offset
076e8a3f 7703 * @param object options
afa0b934 7704 * @param boolean isSimpleSorting
2a16194a 7705 * @param object additionalParameters
ee1a6ccb 7706 */
2a16194a
AE
7707 init: function(containerID, className, offset, options, isSimpleSorting, additionalParameters) {
7708 this._additionalParameters = additionalParameters || { };
ee1a6ccb
AE
7709 this._containerID = $.wcfEscapeID(containerID);
7710 this._container = $('#' + this._containerID);
7711 this._className = className;
b007ce67 7712 this._offset = (offset) ? offset : 0;
ee1a6ccb
AE
7713 this._proxy = new WCF.Action.Proxy({
7714 success: $.proxy(this._success, this)
7715 });
7716 this._structure = { };
7717
7718 // init sortable
076e8a3f 7719 this._options = $.extend(true, {
9dcf11e2 7720 axis: 'y',
afa0b934 7721 connectWith: '#' + this._containerID + ' .sortableList',
3d6b3f29 7722 disableNesting: 'sortableNoNesting',
2d292d09 7723 doNotClear: true,
3d6b3f29 7724 errorClass: 'sortableInvalidTarget',
9dcf11e2
AE
7725 forcePlaceholderSize: true,
7726 helper: 'clone',
3d6b3f29 7727 items: 'li:not(.sortableNoSorting)',
9dcf11e2 7728 opacity: .6,
3d6b3f29 7729 placeholder: 'sortablePlaceholder',
9dcf11e2
AE
7730 tolerance: 'pointer',
7731 toleranceElement: '> span'
076e8a3f 7732 }, options || { });
afa0b934
AE
7733
7734 if (isSimpleSorting) {
7735 $('#' + this._containerID + ' .sortableList').sortable(this._options);
7736 }
7737 else {
2d292d09 7738 $('#' + this._containerID + ' > .sortableList').nestedSortable(this._options);
afa0b934 7739 }
d1a6b448 7740
6d8f21ac 7741 if (this._className) {
a6e646a3
AE
7742 var $formSubmit = this._container.find('.formSubmit');
7743 if (!$formSubmit.length) {
7744 $formSubmit = this._container.next('.formSubmit');
7745 if (!$formSubmit.length) {
7746 console.debug("[WCF.Sortable.Simple] Unable to find form submit for saving, aborting.");
7747 return;
7748 }
7749 }
7750
7751 $formSubmit.children('button[data-type="submit"]').click($.proxy(this._submit, this));
6d8f21ac 7752 }
ee1a6ccb
AE
7753 },
7754
7755 /**
7756 * Saves object structure.
7757 */
d1a6b448 7758 _submit: function() {
2a16194a
AE
7759 // reset structure
7760 this._structure = { };
7761
ee1a6ccb 7762 // build structure
3d6b3f29 7763 this._container.find('.sortableList').each($.proxy(function(index, list) {
ee1a6ccb
AE
7764 var $list = $(list);
7765 var $parentID = $list.data('objectID');
7766
83a29d77
AE
7767 if ($parentID !== undefined) {
7768 $list.children(this._options.items).each($.proxy(function(index, listItem) {
7769 var $objectID = $(listItem).data('objectID');
7770
7771 if (!this._structure[$parentID]) {
7772 this._structure[$parentID] = [ ];
7773 }
7774
7775 this._structure[$parentID].push($objectID);
7776 }, this));
7777 }
ee1a6ccb
AE
7778 }, this));
7779
7780 // send request
2a16194a 7781 var $parameters = $.extend(true, {
2a437fb3
MS
7782 data: {
7783 offset: this._offset,
7784 structure: this._structure
7785 }
2a16194a
AE
7786 }, this._additionalParameters);
7787
7788 this._proxy.setOption('data', {
7789 actionName: 'updatePosition',
7790 className: this._className,
cea5b918 7791 interfaceName: 'wcf\\data\\ISortableAction',
2a16194a 7792 parameters: $parameters
ee1a6ccb
AE
7793 });
7794 this._proxy.sendRequest();
7795 },
7796
7797 /**
7798 * Shows notification upon success.
7799 *
7800 * @param object data
7801 * @param string textStatus
7802 * @param jQuery jqXHR
7803 */
7804 _success: function(data, textStatus, jqXHR) {
7805 if (this._notification === null) {
9b566f66 7806 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.edit'));
ee1a6ccb
AE
7807 }
7808
7809 this._notification.show();
7810 }
3926f3e3 7811});
ee1a6ccb 7812
453aced6
AE
7813WCF.Popover = Class.extend({
7814 /**
7815 * currently active element id
7816 * @var string
7817 */
7818 _activeElementID: '',
7819
2f326e85
AE
7820 /**
7821 * cancels popover
7822 * @var boolean
7823 */
7824 _cancelPopover: false,
7825
453aced6
AE
7826 /**
7827 * element data
7828 * @var object
7829 */
7830 _data: { },
7831
7832 /**
7833 * default dimensions, should reflect the estimated size
7834 * @var object
7835 */
7836 _defaultDimensions: {
523908ab 7837 height: 150,
b85dd964 7838 width: 450
453aced6
AE
7839 },
7840
7841 /**
7842 * default orientation, may be a combintion of left/right and bottom/top
7843 * @var object
7844 */
7845 _defaultOrientation: {
7846 x: 'right',
7847 y: 'top'
7848 },
7849
7850 /**
7851 * delay to show or hide popover, values in miliseconds
7852 * @var object
7853 */
7854 _delay: {
cb22c910 7855 show: 800,
453aced6
AE
7856 hide: 500
7857 },
7858
7859 /**
7860 * true, if an element is being hovered
7861 * @var boolean
7862 */
7863 _hoverElement: false,
7864
7865 /**
7866 * element id of element being hovered
7867 * @var string
7868 */
7869 _hoverElementID: '',
7870
7871 /**
7872 * true, if popover is being hovered
7873 * @var boolean
7874 */
7875 _hoverPopover: false,
7876
7877 /**
7878 * minimum margin (all directions) for popover
7879 * @var integer
7880 */
7881 _margin: 20,
7882
523908ab
AE
7883 /**
7884 * periodical executer once element or popover is no longer being hovered
7885 * @var WCF.PeriodicalExecuter
7886 */
7887 _peOut: null,
7888
453aced6
AE
7889 /**
7890 * periodical executer once an element is being hovered
7891 * @var WCF.PeriodicalExecuter
7892 */
7893 _peOverElement: null,
7894
7895 /**
7896 * popover object
7897 * @var jQuery
7898 */
7899 _popover: null,
7900
5b755307
AE
7901 /**
7902 * popover content
7903 * @var jQuery
7904 */
7905 _popoverContent: null,
7906
453aced6
AE
7907 /**
7908 * popover horizontal offset
7909 * @var integer
7910 */
7911 _popoverOffset: 10,
7912
7913 /**
7914 * element selector
7915 * @var string
7916 */
7917 _selector: '',
7918
7919 /**
7920 * Initializes a new WCF.Popover object.
7921 *
7922 * @param string selector
7923 */
7924 init: function(selector) {
3f42eecb
MW
7925 if ($.browser.mobile) return;
7926
453aced6
AE
7927 // assign default values
7928 this._activeElementID = '';
2f326e85 7929 this._cancelPopover = false;
453aced6
AE
7930 this._data = { };
7931 this._defaultDimensions = {
523908ab
AE
7932 height: 150,
7933 width: 450
453aced6
AE
7934 };
7935 this._defaultOrientation = {
7936 x: 'right',
7937 y: 'top'
7938 };
7939 this._delay = {
cb22c910 7940 show: 800,
453aced6
AE
7941 hide: 500
7942 };
7943 this._hoverElement = false;
7944 this._hoverElementID = '';
7945 this._hoverPopover = false;
7946 this._margin = 20;
523908ab
AE
7947 this._peOut = null;
7948 this._peOverElement = null;
453aced6
AE
7949 this._popoverOffset = 10;
7950 this._selector = selector;
7951
556973c1 7952 this._popover = $('<div class="popover"><span class="icon icon48 icon-spinner"></span><div class="popoverContent"></div></div>').hide().appendTo(document.body);
5b755307 7953 this._popoverContent = this._popover.children('.popoverContent:eq(0)');
523908ab 7954 this._popover.hover($.proxy(this._overPopover, this), $.proxy(this._out, this));
453aced6
AE
7955
7956 this._initContainers();
3c1f2137 7957 WCF.DOMNodeInsertedHandler.addCallback('WCF.Popover.'+selector, $.proxy(this._initContainers, this));
453aced6
AE
7958 },
7959
7960 /**
7961 * Initializes all element triggers.
7962 */
7963 _initContainers: function() {
3f42eecb
MW
7964 if ($.browser.mobile) return;
7965
453aced6
AE
7966 var $elements = $(this._selector);
7967 if (!$elements.length) {
7968 return;
7969 }
7970
7971 $elements.each($.proxy(function(index, element) {
7972 var $element = $(element);
7973 var $elementID = $element.wcfIdentify();
7974
7975 if (!this._data[$elementID]) {
7976 this._data[$elementID] = {
7977 'content': null,
7978 'isLoading': false
7979 };
7980
7981 $element.hover($.proxy(this._overElement, this), $.proxy(this._out, this));
2f326e85 7982
404c9abe 7983 if ($element.is('a') && $element.attr('href')) {
2f326e85
AE
7984 $element.click($.proxy(this._cancel, this));
7985 }
453aced6
AE
7986 }
7987 }, this));
7988 },
7989
2f326e85
AE
7990 /**
7991 * Cancels popovers if link is being clicked
7992 */
7993 _cancel: function(event) {
7994 this._cancelPopover = true;
7995 this._hide(true);
7996 },
7997
453aced6
AE
7998 /**
7999 * Triggered once an element is being hovered.
8000 *
8001 * @param object event
8002 */
8003 _overElement: function(event) {
2f326e85
AE
8004 if (this._cancelPopover) {
8005 return;
8006 }
8007
453aced6
AE
8008 if (this._peOverElement !== null) {
8009 this._peOverElement.stop();
8010 }
8011
8012 var $elementID = $(event.currentTarget).wcfIdentify();
8013 this._hoverElementID = $elementID;
8014 this._peOverElement = new WCF.PeriodicalExecuter($.proxy(function(pe) {
8015 pe.stop();
8016
8017 // still above the same element
8018 if (this._hoverElementID === $elementID) {
8019 this._activeElementID = $elementID;
8020 this._prepare();
8021 }
8022 }, this), this._delay.show);
8023
8024 this._hoverElement = true;
8025 this._hoverPopover = false;
8026 },
8027
8028 /**
8029 * Prepares popover to be displayed.
8030 */
8031 _prepare: function() {
2f326e85
AE
8032 if (this._cancelPopover) {
8033 return;
8034 }
8035
523908ab
AE
8036 if (this._peOut !== null) {
8037 this._peOut.stop();
8038 }
8039
453aced6
AE
8040 // hide and reset
8041 if (this._popover.is(':visible')) {
5b755307 8042 this._hide(true);
453aced6
AE
8043 }
8044
8045 // insert html
8046 if (!this._data[this._activeElementID].loading && this._data[this._activeElementID].content) {
5b755307 8047 this._popoverContent.html(this._data[this._activeElementID].content);
389d57cd 8048
42d7d2cc 8049 WCF.DOMNodeInsertedHandler.execute();
453aced6
AE
8050 }
8051 else {
453aced6
AE
8052 this._data[this._activeElementID].loading = true;
8053 }
8054
8055 // get dimensions
8056 var $dimensions = this._popover.show().getDimensions();
8057 if (this._data[this._activeElementID].loading) {
8058 $dimensions = {
8059 height: Math.max($dimensions.height, this._defaultDimensions.height),
8060 width: Math.max($dimensions.width, this._defaultDimensions.width)
8061 };
8062 }
523908ab
AE
8063 else {
8064 $dimensions = this._fixElementDimensions(this._popover, $dimensions);
8065 }
453aced6
AE
8066 this._popover.hide();
8067
8068 // get orientation
8069 var $orientation = this._getOrientation($dimensions.height, $dimensions.width);
8070 this._popover.css(this._getCSS($orientation.x, $orientation.y));
8071
379272f7
AE
8072 // apply orientation to popover
8073 this._popover.removeClass('bottom left right top').addClass($orientation.x).addClass($orientation.y);
8074
453aced6
AE
8075 this._show();
8076 },
8077
8078 /**
8079 * Displays the popover.
8080 */
8081 _show: function() {
2f326e85
AE
8082 if (this._cancelPopover) {
8083 return;
8084 }
8085
57de5168 8086 this._popover.stop().show().css({ opacity: 1 }).wcfFadeIn();
453aced6 8087
1e5b5b23 8088 if (this._data[this._activeElementID].loading) {
556973c1 8089 this._popover.children('span').show();
1e5b5b23
AE
8090 this._loadContent();
8091 }
5b755307 8092 else {
556973c1 8093 this._popover.children('span').hide();
5b755307
AE
8094 this._popoverContent.css({ opacity: 1 });
8095 }
453aced6
AE
8096 },
8097
8098 /**
8099 * Loads content, should be overwritten by child classes.
8100 */
8101 _loadContent: function() { },
8102
379272f7
AE
8103 /**
8104 * Inserts content and animating transition.
8105 *
8106 * @param string elementID
8107 * @param boolean animate
8108 */
8109 _insertContent: function(elementID, content, animate) {
8110 this._data[elementID] = {
8111 content: content,
8112 loading: false
8113 };
8114
8115 // only update content if element id is active
8116 if (this._activeElementID === elementID) {
1e5b5b23
AE
8117 if (animate) {
8118 // get current dimensions
5b755307 8119 var $dimensions = this._popoverContent.getDimensions();
1e5b5b23
AE
8120
8121 // insert new content
603cfb39
AE
8122 this._popoverContent.css({
8123 height: 'auto',
8124 width: 'auto'
8125 });
5b755307
AE
8126 this._popoverContent.html(this._data[elementID].content);
8127 var $newDimensions = this._popoverContent.getDimensions();
1e5b5b23
AE
8128
8129 // enforce current dimensions and remove HTML
5b755307 8130 this._popoverContent.html('').css({
1e5b5b23
AE
8131 height: $dimensions.height + 'px',
8132 width: $dimensions.width + 'px'
8133 });
8134
8135 // animate to new dimensons
5b755307
AE
8136 var self = this;
8137 this._popoverContent.animate({
1e5b5b23
AE
8138 height: $newDimensions.height + 'px',
8139 width: $newDimensions.width + 'px'
8140 }, 300, function() {
556973c1 8141 self._popover.children('span').hide();
5b755307 8142 self._popoverContent.html(self._data[elementID].content).css({ opacity: 0 }).animate({ opacity: 1 }, 200);
603cfb39 8143
42d7d2cc 8144 WCF.DOMNodeInsertedHandler.execute();
1e5b5b23
AE
8145 });
8146 }
8147 else {
8148 // insert new content
556973c1 8149 this._popover.children('span').hide();
5b755307 8150 this._popoverContent.html(this._data[elementID].content);
42d7d2cc
AE
8151
8152 WCF.DOMNodeInsertedHandler.execute();
1e5b5b23 8153 }
379272f7
AE
8154 }
8155 },
8156
453aced6
AE
8157 /**
8158 * Hides the popover.
8159 */
5b755307
AE
8160 _hide: function(disableAnimation) {
8161 var self = this;
8162 this._popoverContent.stop();
8163 this._popover.stop();
8164
8165 if (disableAnimation) {
57de5168 8166 self._popover.css({ opacity: 0 }).hide();
5b755307
AE
8167 self._popoverContent.empty().css({ height: 'auto', opacity: 0, width: 'auto' });
8168 }
8169 else {
8170 this._popover.wcfFadeOut(function() {
fc821303
AE
8171 self._popoverContent.empty().css({ height: 'auto', opacity: 0, width: 'auto' });
8172 self._popover.hide();
5b755307
AE
8173 });
8174 }
453aced6
AE
8175 },
8176
8177 /**
8178 * Triggered once popover is being hovered.
8179 */
8180 _overPopover: function() {
523908ab
AE
8181 if (this._peOut !== null) {
8182 this._peOut.stop();
8183 }
8184
453aced6
AE
8185 this._hoverElement = false;
8186 this._hoverPopover = true;
8187 },
8188
8189 /**
8190 * Triggered once element *or* popover is now longer hovered.
8191 */
8192 _out: function(event) {
2f326e85
AE
8193 if (this._cancelPopover) {
8194 return;
8195 }
8196
1e5b5b23 8197 this._hoverElementID = '';
453aced6
AE
8198 this._hoverElement = false;
8199 this._hoverPopover = false;
8200
523908ab 8201 this._peOut = new WCF.PeriodicalExecuter($.proxy(function(pe) {
453aced6
AE
8202 pe.stop();
8203
8204 // hide popover is neither element nor popover was hovered given time
8205 if (!this._hoverElement && !this._hoverPopover) {
5b755307 8206 this._hide(false);
453aced6
AE
8207 }
8208 }, this), this._delay.hide);
8209 },
8210
8211 /**
8212 * Resolves popover orientation, tries to use default orientation first.
8213 *
8214 * @param integer height
8215 * @param integer width
8216 * @return object
8217 */
8218 _getOrientation: function(height, width) {
8219 // get offsets and dimensions
8220 var $element = $('#' + this._activeElementID);
523908ab 8221 var $offsets = $element.getOffsets('offset');
453aced6
AE
8222 var $elementDimensions = $element.getDimensions();
8223 var $documentDimensions = $(document).getDimensions();
8224
8225 // try default orientation first
8226 var $orientationX = (this._defaultOrientation.x === 'left') ? 'left' : 'right';
8227 var $orientationY = (this._defaultOrientation.y === 'bottom') ? 'bottom' : 'top';
379272f7 8228 var $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
453aced6
AE
8229
8230 if ($result.flawed) {
8231 // try flipping orientationX
8232 $orientationX = ($orientationX === 'left') ? 'right' : 'left';
8233 $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
8234
8235 if ($result.flawed) {
8236 // try flipping orientationY while maintaing original orientationX
8237 $orientationX = ($orientationX === 'right') ? 'left' : 'right';
8238 $orientationY = ($orientationY === 'bottom') ? 'top' : 'bottom';
8239 $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
8240
8241 if ($result.flawed) {
8242 // try flipping both orientationX and orientationY compared to default values
8243 $orientationX = ($orientationX === 'left') ? 'right' : 'left';
8244 $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
8245
8246 if ($result.flawed) {
8247 // fuck this shit, we will use the default orientation
8248 $orientationX = (this._defaultOrientationX === 'left') ? 'left' : 'right';
8249 $orientationY = (this._defaultOrientationY === 'bottom') ? 'bottom' : 'top';
8250 }
8251 }
8252 }
8253 }
8254
8255 return {
8256 x: $orientationX,
8257 y: $orientationY
d27e2667 8258 };
453aced6
AE
8259 },
8260
8261 /**
8262 * Evaluates if popover fits into given orientation.
8263 *
8264 * @param string orientationX
8265 * @param string orientationY
8266 * @param object offsets
8267 * @param object elementDimensions
8268 * @param object documentDimensions
8269 * @param integer height
8270 * @param integer width
8271 * @return object
8272 */
8273 _evaluateOrientation: function(orientationX, orientationY, offsets, elementDimensions, documentDimensions, height, width) {
8274 var $heightDifference = 0, $widthDifference = 0;
8275 switch (orientationX) {
8276 case 'left':
8277 $widthDifference = offsets.left - width;
8278 break;
8279
8280 case 'right':
523908ab 8281 $widthDifference = documentDimensions.width - (offsets.left + width);
453aced6
AE
8282 break;
8283 }
8284
8285 switch (orientationY) {
8286 case 'bottom':
523908ab 8287 $heightDifference = documentDimensions.height - (offsets.top + elementDimensions.height + this._popoverOffset + height);
453aced6
AE
8288 break;
8289
8290 case 'top':
8291 $heightDifference = offsets.top - (height - this._popoverOffset);
8292 break;
8293 }
8294
8295 // check if both difference are above margin
8296 var $flawed = false;
8297 if ($heightDifference < this._margin || $widthDifference < this._margin) {
8298 $flawed = true;
8299 }
8300
8301 return {
8302 flawed: $flawed,
8303 x: $widthDifference,
8304 y: $heightDifference
8305 };
8306 },
8307
8308 /**
8309 * Computes CSS for popover.
8310 *
8311 * @param string orientationX
8312 * @param string orientationY
8313 * @return object
8314 */
8315 _getCSS: function(orientationX, orientationY) {
8316 var $css = {
8317 bottom: 'auto',
8318 left: 'auto',
453aced6
AE
8319 right: 'auto',
8320 top: 'auto'
8321 };
8322
8323 var $element = $('#' + this._activeElementID);
523908ab
AE
8324 var $offsets = $element.getOffsets('offset');
8325 var $elementDimensions = this._fixElementDimensions($element, $element.getDimensions());
379272f7 8326 var $windowDimensions = $(window).getDimensions();
453aced6
AE
8327
8328 switch (orientationX) {
8329 case 'left':
379272f7 8330 $css.right = $windowDimensions.width - ($offsets.left + $elementDimensions.width);
453aced6
AE
8331 break;
8332
8333 case 'right':
8334 $css.left = $offsets.left;
8335 break;
8336 }
8337
8338 switch (orientationY) {
8339 case 'bottom':
8340 $css.top = $offsets.top + ($elementDimensions.height + this._popoverOffset);
8341 break;
8342
8343 case 'top':
379272f7 8344 $css.bottom = $windowDimensions.height - ($offsets.top - this._popoverOffset);
453aced6
AE
8345 break;
8346 }
8347
8348 return $css;
523908ab
AE
8349 },
8350
8351 /**
8352 * Tries to fix dimensions if element is partially hidden (overflow: hidden).
8353 *
8354 * @param jQuery element
8355 * @param object dimensions
8356 * @return dimensions
8357 */
8358 _fixElementDimensions: function(element, dimensions) {
8359 var $parentDimensions = element.parent().getDimensions();
8360
8361 if ($parentDimensions.height < dimensions.height) {
8362 dimensions.height = $parentDimensions.height;
8363 }
8364
8365 if ($parentDimensions.width < dimensions.width) {
8366 dimensions.width = $parentDimensions.width;
8367 }
8368
8369 return dimensions;
453aced6
AE
8370 }
8371});
8372
cede0aec
AE
8373/**
8374 * Provides an extensible item list with built-in search.
8375 *
8376 * @param string itemListSelector
8377 * @param string searchInputSelector
8378 */
d27e2667 8379WCF.EditableItemList = Class.extend({
cede0aec
AE
8380 /**
8381 * allows custom input not recognized by search to be added
8382 * @var boolean
8383 */
8384 _allowCustomInput: false,
8385
8386 /**
8387 * action class name
8388 * @var string
8389 */
8390 _className: '',
8391
8392 /**
8393 * internal data storage
8394 * @var mixed
8395 */
d27e2667 8396 _data: { },
cede0aec
AE
8397
8398 /**
8399 * form container
8400 * @var jQuery
8401 */
8402 _form: null,
8403
8404 /**
8405 * item list container
8406 * @var jQuery
8407 */
d27e2667 8408 _itemList: null,
cede0aec
AE
8409
8410 /**
8411 * current object id
8412 * @var integer
8413 */
8414 _objectID: 0,
8415
8416 /**
8417 * object type id
8418 * @var integer
8419 */
8420 _objectTypeID: 0,
8421
8422 /**
8423 * search controller
8424 * @var WCF.Search.Base
8425 */
d27e2667 8426 _search: null,
cede0aec
AE
8427
8428 /**
8429 * search input element
8430 * @var jQuery
8431 */
d27e2667
AE
8432 _searchInput: null,
8433
cede0aec
AE
8434 /**
8435 * Creates a new WCF.EditableItemList object.
8436 *
8437 * @param string itemListSelector
8438 * @param string searchInputSelector
8439 */
d27e2667
AE
8440 init: function(itemListSelector, searchInputSelector) {
8441 this._itemList = $(itemListSelector);
8442 this._searchInput = $(searchInputSelector);
0579855e 8443 this._data = { };
d27e2667
AE
8444
8445 if (!this._itemList.length || !this._searchInput.length) {
8446 console.debug("[WCF.EditableItemList] Item list and/or search input do not exist, aborting.");
8447 return;
8448 }
8449
cede0aec
AE
8450 this._objectID = this._getObjectID();
8451 this._objectTypeID = this._getObjectTypeID();
8452
d27e2667
AE
8453 // bind item listener
8454 this._itemList.find('.jsEditableItem').click($.proxy(this._click, this));
cede0aec
AE
8455
8456 // create item list
8457 if (!this._itemList.children('ul').length) {
8458 $('<ul />').appendTo(this._itemList);
8459 }
8460 this._itemList = this._itemList.children('ul');
8461
8462 // bind form submit
8463 this._form = this._itemList.parents('form').submit($.proxy(this._submit, this));
8464
8465 if (this._allowCustomInput) {
3289653d
AE
8466 var self = this;
8467 this._searchInput.keydown($.proxy(this._keyDown, this)).on('paste', function() {
8468 setTimeout(function() { self._onPaste(); }, 100);
8469 });
80da9de3 8470 }
7c6f7523
AE
8471
8472 // block form submit through [ENTER]
8473 this._searchInput.parents('.dropdown').data('preventSubmit', true);
80da9de3
AE
8474 },
8475
8476 /**
8477 * Handles the key down event.
8478 *
8479 * @param object event
8480 */
8481 _keyDown: function(event) {
e3423595 8482 // 188 = [,]
7c6f7523 8483 if (event === null || event.which === 188 || event.which === $.ui.keyCode.ENTER) {
6aa97133 8484 if (event !== null && event.which === $.ui.keyCode.ENTER && this._search) {
7c6f7523
AE
8485 if (this._search._itemIndex !== -1) {
8486 return false;
8487 }
8488 }
8489
80da9de3 8490 var $value = $.trim(this._searchInput.val());
e3423595
AE
8491
8492 // read everything left from caret position
928f1a86 8493 if (event && event.which === 188) {
e3423595
AE
8494 $value = $value.substring(0, this._searchInput.getCaret());
8495 }
8496
80da9de3 8497 if ($value === '') {
cede0aec 8498 return true;
80da9de3
AE
8499 }
8500
8501 this.addItem({
8502 objectID: 0,
8503 label: $value
cede0aec 8504 });
80da9de3
AE
8505
8506 // reset input
928f1a86 8507 if (event && event.which === 188) {
e3423595
AE
8508 this._searchInput.val($.trim(this._searchInput.val().substr(this._searchInput.getCaret())));
8509 }
8510 else {
8511 this._searchInput.val('');
8512 }
80da9de3
AE
8513
8514 if (event !== null) {
8515 event.stopPropagation();
8516 }
8517
8518 return false;
cede0aec 8519 }
80da9de3
AE
8520
8521 return true;
d27e2667
AE
8522 },
8523
3289653d
AE
8524 /**
8525 * Handle paste event.
8526 */
8527 _onPaste: function() {
8528 // split content by comma
8529 var $value = $.trim(this._searchInput.val());
8530 $value = $value.split(',');
8531
8532 for (var $i = 0, $length = $value.length; $i < $length; $i++) {
8533 var $label = $.trim($value[$i]);
8534 if ($label === '') {
8535 continue;
8536 }
8537
8538 this.addItem({
8539 objectID: 0,
8540 label: $label
8541 });
8542 }
8543
8544 this._searchInput.val('');
8545 },
8546
d27e2667
AE
8547 /**
8548 * Loads raw data and converts it into internal structure. Override this methods
8549 * in your derived classes.
8550 *
8551 * @param object data
8552 */
8553 load: function(data) { },
8554
cede0aec
AE
8555 /**
8556 * Removes an item on click.
8557 *
8558 * @param object event
8559 * @return boolean
8560 */
d27e2667
AE
8561 _click: function(event) {
8562 var $element = $(event.currentTarget);
8563 var $objectID = $element.data('objectID');
cede0aec 8564 var $label = $element.data('label');
d27e2667 8565
0b9520f4
MS
8566 if (this._search) {
8567 this._search.removeExcludedSearchValue($label);
8568 }
cede0aec 8569 this._removeItem($objectID, $label);
d27e2667
AE
8570
8571 $element.remove();
8572
8573 event.stopPropagation();
8574 return false;
8575 },
8576
cede0aec
AE
8577 /**
8578 * Returns current object id.
8579 *
8580 * @return integer
8581 */
8582 _getObjectID: function() {
8583 return 0;
8584 },
8585
8586 /**
8587 * Returns current object type id.
8588 *
8589 * @return integer
8590 */
8591 _getObjectTypeID: function() {
8592 return 0;
8593 },
8594
8595 /**
8596 * Adds a new item to the list.
8597 *
8598 * @param object data
8599 * @return boolean
8600 */
8601 addItem: function(data) {
8602 if (this._data[data.objectID]) {
8717050f
AE
8603 if (!(data.objectID === 0 && this._allowCustomInput)) {
8604 return true;
8605 }
cede0aec
AE
8606 }
8607
e5f1745c 8608 var $listItem = $('<li class="badge">' + WCF.String.escapeHTML(data.label) + '</li>').data('objectID', data.objectID).data('label', data.label).appendTo(this._itemList);
cede0aec 8609 $listItem.click($.proxy(this._click, this));
d27e2667 8610
0b9520f4
MS
8611 if (this._search) {
8612 this._search.addExcludedSearchValue(data.label);
8613 }
cede0aec
AE
8614 this._addItem(data.objectID, data.label);
8615
8616 return true;
8617 },
8618
4d28f1de
MS
8619 /**
8620 * Clears the list of items.
8621 */
8622 clearList: function() {
8623 this._itemList.children('li').each($.proxy(function(index, element) {
8624 var $element = $(element);
8625
8626 if (this._search) {
8627 this._search.removeExcludedSearchValue($element.data('label'));
8628 }
8629
8630 $element.remove();
8631 this._removeItem($element.data('objectID'), $element.data('label'));
8632 }, this));
8633 },
8634
cede0aec
AE
8635 /**
8636 * Handles form submit, override in your class.
8637 */
80da9de3
AE
8638 _submit: function() {
8639 this._keyDown(null);
8640 },
cede0aec
AE
8641
8642 /**
8643 * Adds an item to internal storage.
8644 *
8645 * @param integer objectID
8646 * @param string label
8647 */
8648 _addItem: function(objectID, label) {
8649 this._data[objectID] = label;
8650 },
8651
8652 /**
8653 * Removes an item from internal storage.
8654 *
8655 * @param integer objectID
8656 * @param string label
8657 */
8658 _removeItem: function(objectID, label) {
8659 delete this._data[objectID];
4d28f1de
MS
8660 },
8661
8662 /**
8663 * Returns the search input field.
8664 *
8665 * @return jQuery
8666 */
8667 getSearchInput: function() {
8668 return this._searchInput;
d27e2667
AE
8669 }
8670});
8671
27c3b95f
AE
8672/**
8673 * Provides a generic sitemap.
8674 */
8675WCF.Sitemap = Class.extend({
8676 /**
8677 * sitemap name cache
9f959ced 8678 * @var array
27c3b95f
AE
8679 */
8680 _cache: [ ],
8681
8682 /**
8683 * dialog overlay
8684 * @var jQuery
8685 */
8686 _dialog: null,
8687
8688 /**
8689 * initialization state
8690 * @var boolean
8691 */
8692 _didInit: false,
8693
8694 /**
8695 * action proxy
8696 * @var WCF.Action.Proxy
8697 */
8698 _proxy: null,
8699
8700 /**
8701 * Initializes the generic sitemap.
8702 */
8703 init: function() {
8704 $('#sitemap').click($.proxy(this._click, this));
8705
8706 this._cache = [ ];
8707 this._dialog = null;
8708 this._didInit = false;
8709 this._proxy = new WCF.Action.Proxy({
8710 success: $.proxy(this._success, this)
8711 });
8712 },
8713
8714 /**
8715 * Handles clicks on the sitemap icon.
8716 */
8717 _click: function() {
8718 if (this._dialog === null) {
8719 this._dialog = $('<div id="sitemapDialog" />').appendTo(document.body);
8720
8721 this._proxy.setOption('data', {
8722 actionName: 'getSitemap',
8723 className: 'wcf\\data\\sitemap\\SitemapAction'
8724 });
8725 this._proxy.sendRequest();
8726 }
8727 else {
8728 this._dialog.wcfDialog('open');
8729 }
8730 },
8731
8732 /**
8733 * Handles successful AJAX responses.
8734 *
8735 * @param object data
8736 * @param string textStatus
8737 * @param jQuery jqXHR
8738 */
8739 _success: function(data, textStatus, jqXHR) {
8740 if (this._didInit) {
8741 this._cache.push(data.returnValues.sitemapName);
8742
47f83a32 8743 this._dialog.find('#sitemap_' + data.returnValues.sitemapName).html(data.returnValues.template);
27c3b95f
AE
8744
8745 // redraw dialog
8746 this._dialog.wcfDialog('render');
8747 }
8748 else {
8749 // mark sitemap name as loaded
8750 this._cache.push(data.returnValues.sitemapName);
8751
8752 // insert sitemap template
8753 this._dialog.html(data.returnValues.template);
8754
8755 // bind event listener
8756 this._dialog.find('.sitemapNavigation').click($.proxy(this._navigate, this));
8757
7c3a12b1
AE
8758 // select active item
8759 this._dialog.find('.tabMenuContainer').wcfTabs('select', 'sitemap_' + data.returnValues.sitemapName);
8760
27c3b95f
AE
8761 // show dialog
8762 this._dialog.wcfDialog({
5c2036ab 8763 title: WCF.Language.get('wcf.page.sitemap')
27c3b95f
AE
8764 });
8765
8766 this._didInit = true;
8767 }
8768 },
8769
8770 /**
8771 * Navigates between different sitemaps.
8772 *
8773 * @param object event
8774 */
8775 _navigate: function(event) {
8776 var $sitemapName = $(event.currentTarget).data('sitemapName');
8777 if (WCF.inArray($sitemapName, this._cache)) {
47f83a32 8778 this._dialog.find('.tabMenuContainer').wcfTabs('select', 'sitemap_' + $sitemapName);
27c3b95f
AE
8779
8780 // redraw dialog
8781 this._dialog.wcfDialog('render');
8782 }
8783 else {
8784 this._proxy.setOption('data', {
8785 actionName: 'getSitemap',
8786 className: 'wcf\\data\\sitemap\\SitemapAction',
8787 parameters: {
8788 sitemapName: $sitemapName
8789 }
8790 });
8791 this._proxy.sendRequest();
8792 }
8793 }
8794});
8795
a5423278
AE
8796/**
8797 * Provides a language chooser.
8798 *
8799 * @param string containerID
8800 * @param string inputFieldID
8801 * @param integer languageID
8802 * @param object languages
8803 * @param object callback
8804 */
8805WCF.Language.Chooser = Class.extend({
8806 /**
8807 * callback object
8808 * @var object
8809 */
8810 _callback: null,
8811
8812 /**
8813 * dropdown object
8814 * @var jQuery
8815 */
8816 _dropdown: null,
8817
8818 /**
8819 * input field
8820 * @var jQuery
8821 */
8822 _input: null,
8823
8824 /**
8825 * Initializes the language chooser.
8826 *
8827 * @param string containerID
8828 * @param string inputFieldID
8829 * @param integer languageID
8830 * @param object languages
8831 * @param object callback
65b1d6a0 8832 * @param boolean allowEmptyValue
a5423278 8833 */
65b1d6a0 8834 init: function(containerID, inputFieldID, languageID, languages, callback, allowEmptyValue) {
a5423278
AE
8835 var $container = $('#' + containerID);
8836 if ($container.length != 1) {
8837 console.debug("[WCF.Language.Chooser] Invalid container id '" + containerID + "' given");
8838 return;
8839 }
8840
8841 // bind language id input
8842 this._input = $('#' + inputFieldID);
8843 if (!this._input.length) {
8844 this._input = $('<input type="hidden" name="' + inputFieldID + '" value="' + languageID + '" />').appendTo($container);
8845 }
8846
8847 // handle callback
8848 if (callback !== undefined) {
8849 if (!$.isFunction(callback)) {
8850 console.debug("[WCF.Language.Chooser] Given callback is invalid");
8851 return;
8852 }
8853
8854 this._callback = callback;
8855 }
8856
8857 // create language dropdown
8858 this._dropdown = $('<div class="dropdown" id="' + containerID + '-languageChooser" />').appendTo($container);
f3e301ca 8859 $('<div class="dropdownToggle boxFlag box24" data-toggle="' + containerID + '-languageChooser"></div>').appendTo(this._dropdown);
a5423278
AE
8860 var $dropdownMenu = $('<ul class="dropdownMenu" />').appendTo(this._dropdown);
8861
8862 for (var $languageID in languages) {
8863 var $language = languages[$languageID];
635a8feb 8864 var $item = $('<li class="boxFlag"><a class="box24"><div class="framed"><img src="' + $language.iconPath + '" alt="" class="iconFlag" /></div> <div><h3>' + $language.languageName + '</h3></div></a></li>').appendTo($dropdownMenu);
a5423278
AE
8865 $item.data('languageID', $languageID).click($.proxy(this._click, this));
8866
8867 // update dropdown label
8868 if ($languageID == languageID) {
f3e301ca
MW
8869 var $html = $('' + $item.html());
8870 var $innerContent = $html.children().detach();
8871 this._dropdown.children('.dropdownToggle').empty().append($innerContent);
a5423278
AE
8872 }
8873 }
8874
65b1d6a0
AE
8875 // allow an empty selection (e.g. using as language filter)
8876 if (allowEmptyValue) {
8877 $('<li class="dropdownDivider" />').appendTo($dropdownMenu);
8878 var $item = $('<li><a>' + WCF.Language.get('wcf.global.language.noSelection') + '</a></li>').data('languageID', 0).click($.proxy(this._click, this)).appendTo($dropdownMenu);
8879
8880 if (languageID === 0) {
8881 this._dropdown.children('.dropdownToggle').empty().append($item.html());
8882 }
8883 }
8884
a5423278
AE
8885 WCF.Dropdown.init();
8886 },
8887
8888 /**
8889 * Handles click events.
8890 *
8891 * @param object event
8892 */
8893 _click: function(event) {
8894 var $item = $(event.currentTarget);
65b1d6a0 8895 var $languageID = $item.data('languageID');
a5423278
AE
8896
8897 // update input field
65b1d6a0 8898 this._input.val($languageID);
a5423278
AE
8899
8900 // update dropdown label
f3e301ca 8901 var $html = $('' + $item.html());
65b1d6a0 8902 var $innerContent = ($languageID === 0) ? $html : $html.children().detach();
f3e301ca 8903 this._dropdown.children('.dropdownToggle').empty().append($innerContent);
a5423278
AE
8904
8905 // execute callback
8906 if (this._callback !== null) {
8907 this._callback($item);
8908 }
8909 }
8910});
8911
83736ee3
AE
8912/**
8913 * Namespace for style related classes.
8914 */
8915WCF.Style = { };
8916
8917/**
8918 * Provides a visual style chooser.
8919 */
8920WCF.Style.Chooser = Class.extend({
8921 /**
8922 * dialog overlay
8923 * @var jQuery
8924 */
8925 _dialog: null,
8926
8927 /**
8928 * action proxy
8929 * @var WCF.Action.Proxy
8930 */
8931 _proxy: null,
8932
8933 /**
8934 * Initializes the style chooser class.
8935 */
8936 init: function() {
c275a0f3 8937 $('<li class="styleChooser"><a>' + WCF.Language.get('wcf.style.changeStyle') + '</a></li>').appendTo($('#footerNavigation > ul.navigationItems')).click($.proxy(this._showDialog, this));
83736ee3
AE
8938
8939 this._proxy = new WCF.Action.Proxy({
8940 success: $.proxy(this._success, this)
8941 });
8942 },
8943
8944 /**
8945 * Displays the style chooser dialog.
8946 */
8947 _showDialog: function() {
8948 if (this._dialog === null) {
8949 this._dialog = $('<div id="styleChooser" />').hide().appendTo(document.body);
8950 this._loadDialog();
8951 }
8952 else {
8953 this._dialog.wcfDialog({
8954 title: WCF.Language.get('wcf.style.changeStyle')
8955 });
8956 }
8957 },
8958
8959 /**
8960 * Loads the style chooser dialog.
8961 */
8962 _loadDialog: function() {
8963 this._proxy.setOption('data', {
8964 actionName: 'getStyleChooser',
8965 className: 'wcf\\data\\style\\StyleAction'
8966 });
8967 this._proxy.sendRequest();
8968 },
8969
8970 /**
8971 * Handles successful AJAX requests.
8972 *
8973 * @param object data
8974 * @param string textStatus
8975 * @param jQuery jqXHR
8976 */
8977 _success: function(data, textStatus, jqXHR) {
ab7de4fd 8978 if (data.actionName === 'changeStyle') {
83736ee3
AE
8979 window.location.reload();
8980 return;
8981 }
8982
8983 this._dialog.html(data.returnValues.template);
8984 this._dialog.find('li').addClass('pointer').click($.proxy(this._click, this));
8985
8986 this._showDialog();
8987 },
8988
8989 /**
8990 * Changes user style.
8991 *
8992 * @param object event
8993 */
8994 _click: function(event) {
8995 this._proxy.setOption('data', {
8996 actionName: 'changeStyle',
8997 className: 'wcf\\data\\style\\StyleAction',
8998 objectIDs: [ $(event.currentTarget).data('styleID') ]
8999 });
9000 this._proxy.sendRequest();
9001 }
9002});
9003
271dbf28
AE
9004/**
9005 * Converts static user panel items into interactive dropdowns.
9006 *
9007 * @param string containerID
9008 */
9009WCF.UserPanel = Class.extend({
9010 /**
9011 * target container
9012 * @var jQuery
9013 */
9014 _container: null,
9015
9016 /**
9017 * initialization state
9018 * @var boolean
9019 */
9020 _didLoad: false,
9021
9022 /**
9023 * original link element
9024 * @var jQuery
9025 */
9026 _link: null,
9027
531e7fb1
AE
9028 /**
9029 * language variable name for 'no items'
9030 * @var string
9031 */
9032 _noItems: '',
9033
271dbf28
AE
9034 /**
9035 * reverts to original link if return values are empty
9036 * @var boolean
9037 */
9038 _revertOnEmpty: true,
9039
9040 /**
9041 * Initialites the WCF.UserPanel class.
9042 *
9043 * @param string containerID
9044 */
9045 init: function(containerID) {
9046 this._container = $('#' + containerID);
9047 this._didLoad = false;
9048 this._revertOnEmpty = true;
9049
9050 if (this._container.length != 1) {
9051 console.debug("[WCF.UserPanel] Unable to find container identfied by '" + containerID + "', aborting.");
9052 return;
9053 }
9054
531e7fb1 9055 this._convert();
271dbf28
AE
9056 },
9057
9058 /**
9059 * Converts link into an interactive dropdown menu.
9060 */
9061 _convert: function() {
271dbf28
AE
9062 this._container.addClass('dropdown');
9063 this._link = this._container.children('a').remove();
9064
3ef8dee9 9065 var $button = $('<a class="dropdownToggle">' + this._link.html() + '</a>').appendTo(this._container).click($.proxy(this._click, this));
271dbf28
AE
9066 var $dropdownMenu = $('<ul class="dropdownMenu" />').appendTo(this._container);
9067 $('<li class="jsDropdownPlaceholder"><span>' + WCF.Language.get('wcf.global.loading') + '</span></li>').appendTo($dropdownMenu);
9068
9069 this._addDefaultItems($dropdownMenu);
531e7fb1
AE
9070
9071 this._container.dblclick($.proxy(function() {
9072 window.location = this._link.attr('href');
9073 return false;
9074 }, this));
3ef8dee9
AE
9075
9076 WCF.Dropdown.initDropdown($button, false);
271dbf28
AE
9077 },
9078
9079 /**
9080 * Adds default items to dropdown menu.
9081 *
9082 * @param jQuery dropdownMenu
9083 */
9084 _addDefaultItems: function(dropdownMenu) { },
9085
9086 /**
9087 * Adds a dropdown divider.
9088 *
9089 * @param jQuery dropdownMenu
9090 */
9091 _addDivider: function(dropdownMenu) {
9092 $('<li class="dropdownDivider" />').appendTo(dropdownMenu);
9093 },
9094
9095 /**
9096 * Handles clicks on the dropdown item.
9097 */
9098 _click: function() {
9099 if (this._didLoad) {
9100 return;
9101 }
9102
9103 new WCF.Action.Proxy({
9104 autoSend: true,
9105 data: this._getParameters(),
9106 success: $.proxy(this._success, this)
9107 });
9108
9109 this._didLoad = true;
9110 },
9111
9112 /**
9113 * Returns a list of parameters for AJAX request.
9114 *
9115 * @return object
9116 */
9117 _getParameters: function() {
9118 return { };
9119 },
9120
9121 /**
9122 * Handles successful AJAX requests.
9123 *
9124 * @param object data
9125 * @param string textStatus
9126 * @param jQuery jqXHR
9127 */
9128 _success: function(data, textStatus, jqXHR) {
3ef8dee9 9129 var $dropdownMenu = WCF.Dropdown.getDropdownMenu(this._container.wcfIdentify());
531e7fb1
AE
9130 $dropdownMenu.children('.jsDropdownPlaceholder').remove();
9131
271dbf28 9132 if (data.returnValues && data.returnValues.template) {
271dbf28 9133 $('' + data.returnValues.template).prependTo($dropdownMenu);
5d10caf8
AE
9134
9135 // update badge
9136 var $badge = this._container.find('.badge');
9137 if (!$badge.length) {
9138 $badge = $('<span class="badge badgeInverse" />').appendTo(this._container.children('.dropdownToggle'));
e6a05421 9139 $badge.before(' ');
5d10caf8
AE
9140 }
9141 $badge.html(data.returnValues.totalCount);
9142
1682f3cb 9143 this._after($dropdownMenu);
271dbf28
AE
9144 }
9145 else {
531e7fb1 9146 $('<li><span>' + WCF.Language.get(this._noItems) + '</span></li>').prependTo($dropdownMenu);
271dbf28
AE
9147
9148 // remove badge
9149 this._container.find('.badge').remove();
9150 }
1682f3cb
AE
9151 },
9152
9153 /**
9154 * Execute actions after the dropdown menu has been populated.
9155 *
9156 * @param object dropdownMenu
9157 */
9158 _after: function(dropdownMenu) { }
271dbf28
AE
9159});
9160
25763f41
AE
9161/**
9162 * WCF implementation for dialogs, based upon ideas by jQuery UI.
9163 */
9164$.widget('ui.wcfDialog', {
9165 /**
9166 * close button
9167 * @var jQuery
9168 */
9169 _closeButton: null,
9f959ced 9170
25763f41
AE
9171 /**
9172 * dialog container
9173 * @var jQuery
9174 */
9175 _container: null,
9f959ced 9176
25763f41
AE
9177 /**
9178 * dialog content
9179 * @var jQuery
9180 */
9181 _content: null,
9f959ced 9182
25763f41
AE
9183 /**
9184 * modal overlay
9185 * @var jQuery
9186 */
9187 _overlay: null,
9f959ced 9188
25763f41
AE
9189 /**
9190 * plain html for title
9191 * @var string
9192 */
9193 _title: null,
9f959ced 9194
25763f41
AE
9195 /**
9196 * title bar
9197 * @var jQuery
9198 */
9199 _titlebar: null,
9f959ced 9200
25763f41
AE
9201 /**
9202 * dialog visibility state
9203 * @var boolean
9204 */
9205 _isOpen: false,
9f959ced 9206
25763f41
AE
9207 /**
9208 * option list
9209 * @var object
9210 */
9211 options: {
9212 // dialog
9213 autoOpen: true,
9214 closable: true,
9215 closeButtonLabel: null,
af7da802
AE
9216 closeConfirmMessage: null,
9217 closeViaModal: true,
25763f41
AE
9218 hideTitle: false,
9219 modal: true,
9220 title: '',
184a8d6d 9221 zIndex: 400,
9f959ced 9222
34e158d9
AE
9223 // event callbacks
9224 onClose: null,
9225 onShow: null
25763f41 9226 },
9f959ced 9227
38fc45af
AE
9228 /**
9229 * @see $.widget._createWidget()
9230 */
9231 _createWidget: function(options, element) {
9232 // ignore script tags
9233 if ($(element).getTagName() === 'script') {
9234 console.debug("[ui.wcfDialog] Ignored script tag");
bd16ec38 9235 this.element = false;
38fc45af
AE
9236 return null;
9237 }
9238
9239 $.Widget.prototype._createWidget.apply(this, arguments);
9240 },
9241
25763f41
AE
9242 /**
9243 * Initializes a new dialog.
9244 */
9245 _init: function() {
25763f41
AE
9246 if (this.options.autoOpen) {
9247 this.open();
9248 }
9f959ced 9249
25763f41 9250 // act on resize
6e3d3bf1 9251 $(window).resize($.proxy(this._resize, this));
25763f41 9252 },
9f959ced 9253
25763f41
AE
9254 /**
9255 * Creates a new dialog instance.
9256 */
9257 _create: function() {
4445e78f
AE
9258 if (this.options.closeButtonLabel === null) {
9259 this.options.closeButtonLabel = WCF.Language.get('wcf.global.button.close');
9260 }
9261
25763f41 9262 // create dialog container
f74219d0
MK
9263 this._container = $('<div class="dialogContainer" />').hide().css({ zIndex: this.options.zIndex }).appendTo(document.body);
9264 this._titlebar = $('<header class="dialogTitlebar" />').hide().appendTo(this._container);
9265 this._title = $('<span class="dialogTitle" />').hide().appendTo(this._titlebar);
4445e78f 9266 this._closeButton = $('<a class="dialogCloseButton jsTooltip" title="' + this.options.closeButtonLabel + '"><span /></a>').click($.proxy(this.close, this)).hide().appendTo(this._titlebar);
f74219d0 9267 this._content = $('<div class="dialogContent" />').appendTo(this._container);
25763f41 9268
f74219d0
MK
9269 this._setOption('title', this.options.title);
9270 this._setOption('closable', this.options.closable);
9f959ced 9271
25763f41 9272 // move target element into content
6e3d3bf1 9273 var $content = this.element.detach();
25763f41 9274 this._content.html($content);
9f959ced 9275
25763f41
AE
9276 // create modal view
9277 if (this.options.modal) {
5a1b4042
AE
9278 this._overlay = $('#jsWcfDialogOverlay');
9279 if (!this._overlay.length) {
af2ddf45 9280 this._overlay = $('<div id="jsWcfDialogOverlay" class="dialogOverlay" />').css({ height: '100%', zIndex: 399 }).hide().appendTo(document.body);
5a1b4042
AE
9281 }
9282
af7da802 9283 if (this.options.closable && this.options.closeViaModal) {
25763f41
AE
9284 this._overlay.click($.proxy(this.close, this));
9285
9286 $(document).keyup($.proxy(function(event) {
9287 if (event.keyCode && event.keyCode === $.ui.keyCode.ESCAPE) {
9288 this.close();
9289 event.preventDefault();
9290 }
9291 }, this));
9292 }
9293 }
4445e78f 9294
42d7d2cc 9295 WCF.DOMNodeInsertedHandler.execute();
25763f41 9296 },
ef097134 9297
f74219d0
MK
9298 /**
9299 * Sets the given option to the given value.
9300 * See the jQuery UI widget documentation for more.
9301 */
9302 _setOption: function(key, value) {
9303 this.options[key] = value;
9304
9305 if (key == 'hideTitle' || key == 'title') {
9306 if (!this.options.hideTitle && this.options.title != '') {
9307 this._title.html(this.options.title).show();
9308 } else {
9309 this._title.html('');
9310 }
9311 } else if (key == 'closable' || key == 'closeButtonLabel') {
9312 if (this.options.closable) {
9313 this._closeButton.attr('title', this.options.closeButtonLabel).show().find('span').html(this.options.closeButtonLabel);
49b2955d 9314
42d7d2cc 9315 WCF.DOMNodeInsertedHandler.execute();
f74219d0
MK
9316 } else {
9317 this._closeButton.hide();
9318 }
9319 }
9320
9321 if ((!this.options.hideTitle && this.options.title != '') || this.options.closable) {
9322 this._titlebar.show();
9323 } else {
9324 this._titlebar.hide();
9325 }
9326
9327 return this;
9328 },
9329
25763f41
AE
9330 /**
9331 * Opens this dialog.
9332 */
9333 open: function() {
bd16ec38
AE
9334 // ignore script tags
9335 if (this.element === false) {
9336 return;
9337 }
9338
25763f41
AE
9339 if (this.isOpen()) {
9340 return;
9341 }
9f959ced 9342
1e7d6513 9343 if (this._overlay !== null) {
5a1b4042
AE
9344 WCF.activeDialogs++;
9345
9346 if (WCF.activeDialogs === 1) {
9347 this._overlay.show();
9348 }
1e7d6513 9349 }
9f959ced 9350
25763f41
AE
9351 this.render();
9352 this._isOpen = true;
9353 },
9f959ced 9354
25763f41 9355 /**
28410a97 9356 * Returns true if dialog is visible.
25763f41
AE
9357 *
9358 * @return boolean
9359 */
9360 isOpen: function() {
9361 return this._isOpen;
9362 },
ef097134 9363
25763f41
AE
9364 /**
9365 * Closes this dialog.
c2b4d92d 9366 *
8a369709
MS
9367 * This function can be manually called, even if the dialog is set as not
9368 * closable by the user.
9369 *
c2b4d92d 9370 * @param object event
25763f41 9371 */
c2b4d92d 9372 close: function(event) {
8a369709 9373 if (!this.isOpen()) {
25763f41
AE
9374 return;
9375 }
9f959ced 9376
af7da802
AE
9377 if (this.options.closeConfirmMessage) {
9378 WCF.System.Confirmation.show(this.options.closeConfirmMessage, $.proxy(function(action) {
9379 if (action === 'confirm') {
9380 this._close();
9381 }
9382 }, this));
9383 }
9384 else {
9385 this._close();
9386 }
9387
9388 if (event !== undefined) {
9389 event.preventDefault();
9390 }
9391 },
9392
9393 /**
9394 * Handles dialog closing, should never be called directly.
9395 *
9396 * @see $.ui.wcfDialog.close()
9397 */
9398 _close: function() {
25763f41
AE
9399 this._isOpen = false;
9400 this._container.wcfFadeOut();
9f959ced 9401
25763f41 9402 if (this._overlay !== null) {
5a1b4042
AE
9403 WCF.activeDialogs--;
9404
9405 if (WCF.activeDialogs === 0) {
9406 this._overlay.hide();
9407 }
25763f41 9408 }
34e158d9
AE
9409
9410 if (this.options.onClose !== null) {
9411 this.options.onClose();
9412 }
25763f41 9413 },
ef097134 9414
6e3d3bf1
AE
9415 /**
9416 * Renders dialog on resize if visible.
9417 */
9418 _resize: function() {
9419 if (this.isOpen()) {
9420 this.render();
9421 }
9422 },
9423
25763f41
AE
9424 /**
9425 * Renders this dialog, should be called whenever content is updated.
9426 */
4da866ad 9427 render: function() {
caa67386
AE
9428 // check if this if dialog was previously hidden and container is fixed
9429 // at 0px (mobile optimization), in this case scroll to top
9430 if (!this._container.is(':visible') && this._container.css('top') === '0px') {
9431 window.scrollTo(0, 0);
9432 }
9433
942d8ea7
AE
9434 // force dialog and it's contents to be visible
9435 this._container.show();
9436 this._content.children().show();
9f959ced 9437
942d8ea7
AE
9438 // remove fixed content dimensions for calculation
9439 this._content.css({
9440 height: 'auto',
9441 width: 'auto'
abe2607e 9442 });
9f959ced 9443
942d8ea7
AE
9444 // terminate concurrent rendering processes
9445 this._container.stop();
9446 this._content.stop();
9447
9448 // set dialog to be fully opaque, prevents weird bugs in WebKit
9449 this._container.show().css('opacity', 1.0);
9f959ced 9450
942d8ea7
AE
9451 // handle positioning of form submit controls
9452 var $heightDifference = 0;
a41af14a 9453 if (this._content.find('.formSubmit').length) {
942d8ea7
AE
9454 $heightDifference = this._content.find('.formSubmit').outerHeight();
9455
9456 this._content.addClass('dialogForm').css({ marginBottom: $heightDifference + 'px' });
a41af14a
MW
9457 }
9458 else {
9a9c5d2c 9459 this._content.removeClass('dialogForm').css({ marginBottom: '0px' });
a41af14a
MW
9460 }
9461
7200f12a 9462 // force 800px or 90% width
25763f41 9463 var $windowDimensions = $(window).getDimensions();
7200f12a
AE
9464 if ($windowDimensions.width * 0.9 > 800) {
9465 this._container.css('maxWidth', '800px');
845f9d84
AE
9466 }
9467
9468 // calculate dimensions
25763f41 9469 var $containerDimensions = this._container.getDimensions('outer');
d7d9f722 9470 var $contentDimensions = this._content.getDimensions();
9f959ced 9471
abe2607e
AE
9472 // calculate maximum content height
9473 var $heightDifference = $containerDimensions.height - $contentDimensions.height;
49b2955d 9474 var $maximumHeight = $windowDimensions.height - $heightDifference - 120;
abe2607e
AE
9475 this._content.css({ maxHeight: $maximumHeight + 'px' });
9476
0ebf91e4 9477 this._determineOverflow();
9f959ced 9478
9acd5123 9479 // calculate new dimensions
0ebf91e4 9480 $containerDimensions = this._container.getDimensions('outer');
9f959ced 9481
25763f41
AE
9482 // move container
9483 var $leftOffset = Math.round(($windowDimensions.width - $containerDimensions.width) / 2);
9484 var $topOffset = Math.round(($windowDimensions.height - $containerDimensions.height) / 2);
9f959ced 9485
25763f41
AE
9486 // place container at 20% height if possible
9487 var $desiredTopOffset = Math.round(($windowDimensions.height / 100) * 20);
9488 if ($desiredTopOffset < $topOffset) {
9489 $topOffset = $desiredTopOffset;
9490 }
9f959ced 9491
942d8ea7
AE
9492 // apply offset
9493 this._container.css({
9494 left: $leftOffset + 'px',
9495 top: $topOffset + 'px'
9496 });
9497
9498 // remove static dimensions
9499 this._content.css({
9500 height: 'auto',
942d8ea7
AE
9501 width: 'auto'
9502 });
9503
9504 if (!this.isOpen()) {
25763f41
AE
9505 // hide container again
9506 this._container.hide();
9f959ced 9507
25763f41 9508 // fade in container
9acd5123 9509 this._container.wcfFadeIn($.proxy(function() {
942d8ea7
AE
9510 if (this.options.onShow !== null) {
9511 this.options.onShow();
9512 }
79537c05 9513 }, this));
9acd5123 9514 }
9acd5123 9515 },
9f959ced 9516
c1b31f6f
AE
9517 /**
9518 * Determines content overflow based upon static dimensions.
9519 */
9520 _determineOverflow: function() {
9521 var $max = $(window).getDimensions();
9522 var $maxHeight = this._content.css('maxHeight');
9523 this._content.css('maxHeight', 'none');
9524 var $dialog = this._container.getDimensions('outer');
9525
9526 var $overflow = 'visible';
9527 if (($max.height * 0.8 < $dialog.height) || ($max.width * 0.8 < $dialog.width)) {
9528 $overflow = 'auto';
9529 }
9530
9531 this._content.css('overflow', $overflow);
9532 this._content.css('maxHeight', $maxHeight);
c68d2056
AE
9533
9534 if ($overflow === 'visible') {
9535 // content may already overflow, even though the overall height is still below the threshold
9536 var $contentHeight = 0;
9537 this._content.children().each(function(index, child) {
9538 $contentHeight += $(child).outerHeight();
9539 });
9540
9541 if (this._content.height() < $contentHeight) {
9542 this._content.css('overflow', 'auto');
9543 }
9544 }
c1b31f6f
AE
9545 },
9546
9acd5123
AE
9547 /**
9548 * Returns calculated content dimensions.
9549 *
9550 * @param integer maximumHeight
9551 * @return object
9552 */
9553 _getContentDimensions: function(maximumHeight) {
9554 var $contentDimensions = this._content.getDimensions();
9f959ced 9555
9acd5123 9556 // set height to maximum height if exceeded
942d8ea7 9557 if (maximumHeight && $contentDimensions.height > maximumHeight) {
9acd5123 9558 $contentDimensions.height = maximumHeight;
25763f41 9559 }
9f959ced 9560
9acd5123 9561 return $contentDimensions;
25763f41
AE
9562 }
9563});
9564
848d0782
AE
9565/**
9566 * Provides a slideshow for lists.
9567 */
9568$.widget('ui.wcfSlideshow', {
9569 /**
9570 * button list object
9571 * @var jQuery
9572 */
9573 _buttonList: null,
9574
9575 /**
9576 * number of items
9577 * @var integer
9578 */
9579 _count: 0,
9580
9581 /**
9582 * item index
9583 * @var integer
9584 */
9585 _index: 0,
9586
9587 /**
9588 * item list object
9589 * @var jQuery
9590 */
9591 _itemList: null,
9592
9593 /**
9594 * list of items
9595 * @var jQuery
9596 */
9597 _items: null,
9598
9599 /**
9600 * timer object
9601 * @var WCF.PeriodicalExecuter
9602 */
9603 _timer: null,
9604
9605 /**
9606 * list item width
9607 * @var integer
9608 */
9609 _width: 0,
9610
9611 /**
9612 * list of options
9613 * @var object
9614 */
9615 options: {
9616 /* enables automatic cycling of items */
9617 cycle: true,
9618 /* cycle interval in seconds */
9619 cycleInterval: 5,
9620 /* gap between items in pixels */
9621 itemGap: 50,
9622 },
9623
9624 /**
9625 * Creates a new instance of ui.wcfSlideshow.
9626 */
9627 _create: function() {
9628 this._itemList = this.element.children('ul');
9629 this._items = this._itemList.children('li');
9630 this._count = this._items.length;
9631 this._index = 0;
9632
b567c0c6
MW
9633 if (this._count > 1) {
9634 this._initSlideshow();
9635 }
848d0782
AE
9636 },
9637
9638 /**
9639 * Initializes the slideshow.
9640 */
9641 _initSlideshow: function() {
9642 // calculate item dimensions
9643 var $itemHeight = $(this._items.get(0)).outerHeight();
9644 this._items.addClass('slideshowItem');
9645 this._width = this.element.css('height', $itemHeight).innerWidth();
9646 this._itemList.addClass('slideshowItemList').css('left', 0);
9647
9648 this._items.each($.proxy(function(index, item) {
9649 $(item).show().css({
9650 height: $itemHeight,
9651 left: ((this._width + this.options.itemGap) * index),
9652 width: this._width
9653 });
9654 }, this));
9655
9656 this.element.css({
9657 height: $itemHeight,
9658 width: this._width
9659 }).hover($.proxy(this._hoverIn, this), $.proxy(this._hoverOut, this));
9660
9661 // create toggle buttons
b567c0c6
MW
9662 this._buttonList = $('<ul class="slideshowButtonList" />').appendTo(this.element);
9663 for (var $i = 0; $i < this._count; $i++) {
9664 var $link = $('<li><a><span class="icon icon16 icon-circle" /></a></li>').data('index', $i).click($.proxy(this._click, this)).appendTo(this._buttonList);
9665 if ($i == 0) {
9666 $link.find('.icon').addClass('active');
848d0782
AE
9667 }
9668 }
9669
b567c0c6
MW
9670 this._resetTimer();
9671
848d0782
AE
9672 $(window).resize($.proxy(this._resize, this));
9673 },
9674
9675 /**
9676 * Handles browser resizing
9677 */
9678 _resize: function() {
9679 this._width = this.element.css('width', 'auto').innerWidth();
9680 this._items.each($.proxy(function(index, item) {
9681 $(item).css({
9682 left: ((this._width + this.options.itemGap) * index),
9683 width: this._width
9684 });
9685 }, this));
9686
9687 this._index--;
9688 this.moveTo(null);
9689 },
9690
9691 /**
9692 * Disables cycling while hovering.
9693 */
9694 _hoverIn: function() {
9695 if (this._timer !== null) {
9696 this._timer.stop();
9697 }
9698 },
9699
9700 /**
9701 * Enables cycling after mouse out.
9702 */
9703 _hoverOut: function() {
9704 this._resetTimer();
9705 },
9706
9707 /**
9708 * Resets cycle timer.
9709 */
9710 _resetTimer: function() {
9711 if (!this.options.cycle) {
9712 return;
9713 }
9714
9715 if (this._timer !== null) {
9716 this._timer.stop();
9717 }
9718
9719 var self = this;
9720 this._timer = new WCF.PeriodicalExecuter(function() {
9721 self.moveTo(null);
9722 }, this.options.cycleInterval * 1000);
9723 },
9724
9725 /**
9726 * Handles clicks on the select buttons.
9727 *
9728 * @param object event
9729 */
9730 _click: function(event) {
9731 this.moveTo($(event.currentTarget).data('index'));
9732
9733 this._resetTimer();
9734 },
9735
9736 /**
9737 * Moves to a specified item index, NULL will move to the next item in list.
9738 *
9739 * @param integer index
9740 */
9741 moveTo: function(index) {
9742 this._index = (index === null) ? this._index + 1 : index;
9743 if (this._index == this._count) {
9744 this._index = 0;
9745 }
9746
9747 $(this._buttonList.find('.icon').removeClass('active').get(this._index)).addClass('active');
9748 this._itemList.css('left', this._index * (this._width + this.options.itemGap) * -1);
9749 }
9750});
9751
158bd3ca 9752/**
18404c0a 9753 * Custom tab menu implementation for WCF.
158bd3ca
TD
9754 */
9755$.widget('ui.wcfTabs', $.ui.tabs, {
18404c0a
AE
9756 /**
9757 * Workaround for ids containing a dot ".", until jQuery UI devs learn
9758 * to properly escape ids ... (it took 18 months until they finally
9759 * fixed it!)
9760 *
9761 * @see http://bugs.jqueryui.com/ticket/4681
9762 * @see $.ui.tabs.prototype._sanitizeSelector()
9763 */
158bd3ca
TD
9764 _sanitizeSelector: function(hash) {
9765 return hash.replace(/([:\.])/g, '\\$1');
18404c0a 9766 },
f24f0823 9767
18404c0a
AE
9768 /**
9769 * @see $.ui.tabs.prototype.select()
9770 */
9771 select: function(index) {
9772 if (!$.isNumeric(index)) {
9773 // panel identifier given
9774 this.panels.each(function(i, panel) {
9775 if ($(panel).wcfIdentify() === index) {
9776 index = i;
9777 return false;
9778 }
9779 });
9780
9781 // unable to identify panel
9782 if (!$.isNumeric(index)) {
9783 console.debug("[ui.wcfTabs] Unable to find panel identified by '" + index + "', aborting.");
9784 return;
9785 }
9786 }
dbd319de 9787
1682f3cb 9788 this._setOption('active', index);
18404c0a 9789 },
9f959ced 9790
22b63e33
AE
9791 /**
9792 * Selects a specific tab by triggering the 'click' event.
9793 *
9794 * @param string tabIdentifier
9795 */
9796 selectTab: function(tabIdentifier) {
9797 tabIdentifier = '#' + tabIdentifier;
9798
9799 this.anchors.each(function(index, anchor) {
9800 var $anchor = $(anchor);
9801 if ($anchor.prop('hash') === tabIdentifier) {
9802 $anchor.trigger('click');
9803 return false;
9804 }
9805 });
9806 },
9807
18404c0a
AE
9808 /**
9809 * Returns the currently selected tab index.
9810 *
9811 * @return integer
9812 */
9813 getCurrentIndex: function() {
f4126129 9814 return this.lis.index(this.lis.filter('.ui-tabs-selected'));
f24f0823
AE
9815 },
9816
9817 /**
28410a97 9818 * Returns true if identifier is used by an anchor.
f24f0823
AE
9819 *
9820 * @param string identifier
9821 * @param boolean isChildren
9822 * @return boolean
9823 */
9824 hasAnchor: function(identifier, isChildren) {
9825 var $matches = false;
9826
9827 this.anchors.each(function(index, anchor) {
9828 var $href = $(anchor).attr('href');
9829 if (/#.+/.test($href)) {
9830 // split by anchor
9831 var $parts = $href.split('#', 2);
9832 if (isChildren) {
9833 $parts = $parts[1].split('-', 2);
9834 }
9835
9836 if ($parts[1] === identifier) {
9837 $matches = true;
9838
9839 // terminate loop
9840 return false;
9841 }
9842 }
9843 });
9844
9845 return $matches;
3d57a2dd
AE
9846 },
9847
9848 /**
9849 * Shows default tab.
9850 */
9851 revertToDefault: function() {
9852 var $active = this.element.data('active');
9853 if (!$active || $active === '') $active = 0;
9854
9855 this.select($active);
ef995fd9
AE
9856 },
9857
9858 /**
9859 * @see $.ui.tabs.prototype._processTabs()
9860 */
9861 _processTabs: function() {
9862 var that = this;
7b608580 9863
ef995fd9
AE
9864 this.tablist = this._getList()
9865 .addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
9866 .attr( "role", "tablist" );
7b608580 9867
ef995fd9
AE
9868 this.tabs = this.tablist.find( "> li:has(a[href])" )
9869 .addClass( "ui-state-default ui-corner-top" )
9870 .attr({
9871 role: "tab",
9872 tabIndex: -1
9873 });
7b608580 9874
ef995fd9
AE
9875 this.anchors = this.tabs.map(function() {
9876 return $( "a", this )[ 0 ];
9877 })
9878 .addClass( "ui-tabs-anchor" )
9879 .attr({
9880 role: "presentation",
9881 tabIndex: -1
9882 });
7b608580 9883
ef995fd9 9884 this.panels = $();
7b608580 9885
ef995fd9
AE
9886 this.anchors.each(function( i, anchor ) {
9887 var selector, panel,
9888 anchorId = $( anchor ).uniqueId().attr( "id" ),
9889 tab = $( anchor ).closest( "li" ),
9890 originalAriaControls = tab.attr( "aria-controls" );
7b608580 9891
ef995fd9
AE
9892 // inline tab
9893 selector = anchor.hash;
9894 panel = that.element.find( that._sanitizeSelector( selector ) );
9895
9896 if ( panel.length) {
9897 that.panels = that.panels.add( panel );
9898 }
9899 if ( originalAriaControls ) {
9900 tab.data( "ui-tabs-aria-controls", originalAriaControls );
9901 }
9902 tab.attr({
9903 "aria-controls": selector.substring( 1 ),
9904 "aria-labelledby": anchorId
9905 });
9906 panel.attr( "aria-labelledby", anchorId );
9907 });
7b608580 9908
ef995fd9
AE
9909 this.panels
9910 .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
9911 .attr( "role", "tabpanel" );
9912 },
9913
9914 /**
9915 * @see $.ui.tabs.prototype.load()
9916 */
9917 load: function( index, event ) {
9918 return;
158bd3ca
TD
9919 }
9920});
9921
9922/**
9923 * jQuery widget implementation of the wcf pagination.
9924 */
9925$.widget('ui.wcfPages', {
9926 SHOW_LINKS: 11,
9927 SHOW_SUB_LINKS: 20,
9928
9929 options: {
9930 // vars
9931 activePage: 1,
9932 maxPage: 1,
9933
158bd3ca
TD
9934 // language
9935 // we use options here instead of language variables, because the paginator is not only usable with pages
9936 nextPage: null,
9c1e5045 9937 previousPage: null
158bd3ca
TD
9938 },
9939
9940 /**
9941 * Creates the pages widget.
9942 */
9943 _create: function() {
9944 if (this.options.nextPage === null) this.options.nextPage = WCF.Language.get('wcf.global.page.next');
9945 if (this.options.previousPage === null) this.options.previousPage = WCF.Language.get('wcf.global.page.previous');
9946
b9698c4c 9947 this.element.addClass('pageNavigation');
158bd3ca
TD
9948
9949 this._render();
9950 },
9951
9952 /**
9953 * Destroys the pages widget.
9954 */
9955 destroy: function() {
9956 $.Widget.prototype.destroy.apply(this, arguments);
9957
9958 this.element.children().remove();
9959 },
9960
9961 /**
ebbf5629 9962 * Renders the pages widget.
158bd3ca
TD
9963 */
9964 _render: function() {
9965 // only render if we have more than 1 page
9966 if (!this.options.disabled && this.options.maxPage > 1) {
3bdc6920
AE
9967 var $hasHiddenPages = false;
9968
158bd3ca
TD
9969 // make sure pagination is visible
9970 if (this.element.hasClass('hidden')) {
9971 this.element.removeClass('hidden');
9972 }
9973 this.element.show();
9974
9975 this.element.children().remove();
9976
556973c1 9977 var $pageList = $('<ul />');
158bd3ca
TD
9978 this.element.append($pageList);
9979
556973c1 9980 var $previousElement = $('<li class="button skip" />');
158bd3ca
TD
9981 $pageList.append($previousElement);
9982
9983 if (this.options.activePage > 1) {
9984 var $previousLink = $('<a' + ((this.options.previousPage != null) ? (' title="' + this.options.previousPage + '"') : ('')) + '></a>');
9985 $previousElement.append($previousLink);
9986 this._bindSwitchPage($previousLink, this.options.activePage - 1);
9987
556973c1 9988 var $previousImage = $('<span class="icon icon16 icon-double-angle-left" />');
158bd3ca
TD
9989 $previousLink.append($previousImage);
9990 }
9991 else {
556973c1 9992 var $previousImage = $('<span class="icon icon16 icon-double-angle-left" />');
158bd3ca 9993 $previousElement.append($previousImage);
d62d0d94 9994 $previousElement.addClass('disabled').removeClass('button');
5814a7f5 9995 $previousImage.addClass('disabled');
158bd3ca 9996 }
158bd3ca
TD
9997
9998 // add first page
9999 $pageList.append(this._renderLink(1));
10000
10001 // calculate page links
10002 var $maxLinks = this.SHOW_LINKS - 4;
10003 var $linksBefore = this.options.activePage - 2;
10004 if ($linksBefore < 0) $linksBefore = 0;
10005 var $linksAfter = this.options.maxPage - (this.options.activePage + 1);
10006 if ($linksAfter < 0) $linksAfter = 0;
10007 if (this.options.activePage > 1 && this.options.activePage < this.options.maxPage) $maxLinks--;
10008
10009 var $half = $maxLinks / 2;
10010 var $left = this.options.activePage;
10011 var $right = this.options.activePage;
10012 if ($left < 1) $left = 1;
10013 if ($right < 1) $right = 1;
10014 if ($right > this.options.maxPage - 1) $right = this.options.maxPage - 1;
10015
10016 if ($linksBefore >= $half) {
10017 $left -= $half;
10018 }
10019 else {
10020 $left -= $linksBefore;
10021 $right += $half - $linksBefore;
10022 }
10023
10024 if ($linksAfter >= $half) {
10025 $right += $half;
10026 }
10027 else {
10028 $right += $linksAfter;
10029 $left -= $half - $linksAfter;
10030 }
10031
10032 $right = Math.ceil($right);
10033 $left = Math.ceil($left);
10034 if ($left < 1) $left = 1;
10035 if ($right > this.options.maxPage) $right = this.options.maxPage;
10036
10037 // left ... links
10038 if ($left > 1) {
10039 if ($left - 1 < 2) {
10040 $pageList.append(this._renderLink(2));
10041 }
10042 else {
ec591c4f 10043 $('<li class="button jumpTo"><a title="' + WCF.Language.get('wcf.global.page.jumpTo') + '" class="jsTooltip">...</a></li>').appendTo($pageList);
3bdc6920 10044 $hasHiddenPages = true;
158bd3ca
TD
10045 }
10046 }
10047
10048 // visible links
10049 for (var $i = $left + 1; $i < $right; $i++) {
10050 $pageList.append(this._renderLink($i));
10051 }
10052
10053 // right ... links
10054 if ($right < this.options.maxPage) {
10055 if (this.options.maxPage - $right < 2) {
10056 $pageList.append(this._renderLink(this.options.maxPage - 1));
10057 }
10058 else {
ec591c4f 10059 $('<li class="button jumpTo"><a title="' + WCF.Language.get('wcf.global.page.jumpTo') + '" class="jsTooltip">...</a></li>').appendTo($pageList);
3bdc6920 10060 $hasHiddenPages = true;
158bd3ca
TD
10061 }
10062 }
10063
10064 // add last page
10065 $pageList.append(this._renderLink(this.options.maxPage));
10066
10067 // add next button
556973c1 10068 var $nextElement = $('<li class="button skip" />');
158bd3ca
TD
10069 $pageList.append($nextElement);
10070
10071 if (this.options.activePage < this.options.maxPage) {
b9698c4c 10072 var $nextLink = $('<a' + ((this.options.nextPage != null) ? (' title="' + this.options.nextPage + '"') : ('')) + '></a>');
158bd3ca
TD
10073 $nextElement.append($nextLink);
10074 this._bindSwitchPage($nextLink, this.options.activePage + 1);
10075
556973c1 10076 var $nextImage = $('<span class="icon icon16 icon-double-angle-right" />');
158bd3ca
TD
10077 $nextLink.append($nextImage);
10078 }
10079 else {
556973c1 10080 var $nextImage = $('<span class="icon icon16 icon-double-angle-right" />');
158bd3ca 10081 $nextElement.append($nextImage);
d62d0d94 10082 $nextElement.addClass('disabled').removeClass('button');
5814a7f5 10083 $nextImage.addClass('disabled');
158bd3ca 10084 }
3bdc6920
AE
10085
10086 if ($hasHiddenPages) {
a19475d3 10087 $pageList.data('pages', this.options.maxPage);
3bdc6920
AE
10088 WCF.System.PageNavigation.init('#' + $pageList.wcfIdentify(), $.proxy(function(pageNo) {
10089 this.switchPage(pageNo);
10090 }, this));
3bdc6920 10091 }
158bd3ca
TD
10092 }
10093 else {
10094 // otherwise hide the paginator if not already hidden
10095 this.element.hide();
10096 }
10097 },
10098
10099 /**
9f959ced 10100 * Renders a page link.
158bd3ca
TD
10101 *
10102 * @parameter integer page
9f959ced 10103 * @return jQuery
158bd3ca
TD
10104 */
10105 _renderLink: function(page, lineBreak) {
b9698c4c 10106 var $pageElement = $('<li class="button"></li>');
158bd3ca
TD
10107 if (lineBreak != undefined && lineBreak) {
10108 $pageElement.addClass('break');
10109 }
10110 if (page != this.options.activePage) {
10111 var $pageLink = $('<a>' + WCF.String.addThousandsSeparator(page) + '</a>');
10112 $pageElement.append($pageLink);
10113 this._bindSwitchPage($pageLink, page);
10114 }
10115 else {
10116 $pageElement.addClass('active');
10117 var $pageSubElement = $('<span>' + WCF.String.addThousandsSeparator(page) + '</span>');
10118 $pageElement.append($pageSubElement);
10119 }
10120
10121 return $pageElement;
10122 },
10123
10124 /**
10125 * Binds the 'click'-event for the page switching to the given element.
10126 *
10127 * @parameter $(element) element
10128 * @paremeter integer page
10129 */
10130 _bindSwitchPage: function(element, page) {
10131 var $self = this;
10132 element.click(function() {
10133 $self.switchPage(page);
10134 });
10135 },
10136
10137 /**
10138 * Switches to the given page
10139 *
10140 * @parameter Event event
10141 * @parameter integer page
10142 */
10143 switchPage: function(page) {
10144 this._setOption('activePage', page);
10145 },
10146
10147 /**
10148 * Sets the given option to the given value.
10149 * See the jQuery UI widget documentation for more.
10150 */
10151 _setOption: function(key, value) {
10152 if (key == 'activePage') {
10153 if (value != this.options[key] && value > 0 && value <= this.options.maxPage) {
10154 // you can prevent the page switching by returning false or by event.preventDefault()
10155 // in a shouldSwitch-callback. e.g. if an AJAX request is already running.
10156 var $result = this._trigger('shouldSwitch', undefined, {
81d1b3b5 10157 nextPage: value
158bd3ca
TD
10158 });
10159
a19475d3 10160 if ($result || $result !== undefined) {
158bd3ca
TD
10161 this.options[key] = value;
10162 this._render();
10163 this._trigger('switched', undefined, {
81d1b3b5 10164 activePage: value
158bd3ca
TD
10165 });
10166 }
10167 else {
10168 this._trigger('notSwitched', undefined, {
81d1b3b5 10169 activePage: value
158bd3ca
TD
10170 });
10171 }
10172 }
10173 }
10174 else {
10175 this.options[key] = value;
10176
10177 if (key == 'disabled') {
10178 if (value) {
10179 this.element.children().remove();
10180 }
10181 else {
f4126129 10182 this._render();
158bd3ca
TD
10183 }
10184 }
10185 else if (key == 'maxPage') {
10186 this._render();
10187 }
10188 }
10189
10190 return this;
10191 },
10192
10193 /**
10194 * Start input of pagenumber
10195 *
10196 * @parameter Event event
10197 */
10198 _startInput: function(event) {
10199 // hide a-tag
10200 var $childLink = $(event.currentTarget);
10201 if (!$childLink.is('a')) $childLink = $childLink.parent('a');
10202
10203 $childLink.hide();
10204
10205 // show input-tag
10206 var $childInput = $childLink.parent('li').children('input')
10207 .css('display', 'block')
10208 .val('');
10209
10210 $childInput.focus();
10211 },
10212
10213 /**
10214 * Stops input of pagenumber
10215 *
10216 * @parameter Event event
10217 */
10218 _stopInput: function(event) {
10219 // hide input-tag
10220 var $childInput = $(event.currentTarget);
10221 $childInput.css('display', 'none');
10222
10223 // show a-tag
f4126129 10224 var $childContainer = $childInput.parent('li');
158bd3ca
TD
10225 if ($childContainer != undefined && $childContainer != null) {
10226 $childContainer.children('a').show();
10227 }
10228 },
10229
10230 /**
10231 * Handles input of pagenumber
10232 *
10233 * @parameter Event event
10234 */
10235 _handleInput: function(event) {
10236 var $ie7 = ($.browser.msie && $.browser.version == '7.0');
10237 if (event.type != 'keyup' || $ie7) {
10238 if (!$ie7 || ((event.which == 13 || event.which == 27) && event.type == 'keyup')) {
10239 if (event.which == 13) {
10240 this.switchPage(parseInt($(event.currentTarget).val()));
10241 }
10242
10243 if (event.which == 13 || event.which == 27) {
10244 this._stopInput(event);
10245 event.stopPropagation();
10246 }
10247 }
10248 }
10249 }
10250});
10251
02e410dc
MW
10252/**
10253 * Namespace for category related classes.
10254 */
10255WCF.Category = { };
10256
10257/**
10258 * Handles selection of categories.
10259 */
10260WCF.Category.NestedList = Class.extend({
10261 /**
10262 * list of categories
10263 * @var object
10264 */
10265 _categories: { },
10266
10267 /**
10268 * Initializes the WCF.Category.NestedList object.
10269 */
10270 init: function() {
10271 var self = this;
10272 $('.jsCategory').each(function(index, category) {
10273 var $category = $(category).data('parentCategoryID', null).change($.proxy(self._updateSelection, self));
10274 self._categories[$category.val()] = $category;
10275
10276 // find child categories
10277 var $childCategoryIDs = [ ];
10278 $category.parents('li').find('.jsChildCategory').each(function(innerIndex, childCategory) {
10279 var $childCategory = $(childCategory).data('parentCategoryID', $category.val()).change($.proxy(self._updateSelection, self));
10280 self._categories[$childCategory.val()] = $childCategory;
10281 $childCategoryIDs.push($childCategory.val());
10282
10283 if ($childCategory.is(':checked')) {
10284 $category.prop('checked', 'checked');
10285 }
10286 });
10287
10288 $category.data('childCategoryIDs', $childCategoryIDs);
10289 });
10290 },
10291
10292 /**
10293 * Updates selection of categories.
10294 *
10295 * @param object event
10296 */
10297 _updateSelection: function(event) {
10298 var $category = $(event.currentTarget);
10299 var $parentCategoryID = $category.data('parentCategoryID');
10300
10301 if ($category.is(':checked')) {
10302 // child category
10303 if ($parentCategoryID !== null) {
10304 // mark parent category as checked
10305 this._categories[$parentCategoryID].prop('checked', 'checked');
10306 }
10307 }
10308 else {
10309 // top-level category
10310 if ($parentCategoryID === null) {
10311 // unmark all child categories
10312 var $childCategoryIDs = $category.data('childCategoryIDs');
10313 for (var $i = 0, $length = $childCategoryIDs.length; $i < $length; $i++) {
10314 this._categories[$childCategoryIDs[$i]].prop('checked', false);
10315 }
10316 }
10317 }
10318 }
10319});
10320
158bd3ca
TD
10321/**
10322 * Encapsulate eval() within an own function to prevent problems
10323 * with optimizing and minifiny JS.
10324 *
10325 * @param mixed expression
10326 * @returns mixed
10327 */
10328function wcfEval(expression) {
10329 return eval(expression);
10330}