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