2 * Class and function collection for WCF.
4 * Major Contributors: Markus Bartz, Tim Duesterhus, Matthias Schmidt and Marcel Werk
6 * @author Alexander Ebert
7 * @copyright 2001-2014 WoltLab GmbH
8 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
12 // store original implementation
13 var $jQueryData
= jQuery
.fn
.data
;
16 * Override jQuery.fn.data() to support custom 'ID' suffix which will
17 * be translated to '-id' at runtime.
19 * @see jQuery.fn.data()
21 jQuery
.fn
.data = function(key
, value
) {
25 for (var $key
in key
) {
26 if ($key
.match(/ID$/)) {
27 var $value
= key
[$key
];
30 $key
= $key
.replace(/ID$/, '-id');
39 if (key
.match(/ID$/)) {
40 arguments
[0] = key
.replace(/ID$/, '-id');
46 // call jQuery's own data method
47 var $data
= $jQueryData
.apply(this, arguments
);
49 // handle .data() call without arguments
50 if (key
=== undefined) {
51 for (var $key
in $data
) {
52 if ($key
.match(/Id$/)) {
53 $data
[$key
.replace(/Id$/, 'ID')] = $data
[$key
];
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') {
67 console
[consoleProperties
[i
]] = function () { };
71 if (typeof(console
.debug
) === 'undefined') {
72 // forward console.debug to console.log (IE9)
73 console
.debug = function(string
) { console
.log(string
); };
78 * Simple JavaScript Inheritance
79 * By John Resig http://ejohn.org/
82 // Inspired by base2 and Prototype
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
;};})();
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 */
86 window
.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"}}}());
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
}})();
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)
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
});
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);
101 //# sourceMappingURL=head.load.min.js.map
105 * Provides a hashCode() method for strings, similar to Java's String.hashCode().
107 * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
109 String.prototype.hashCode = function() {
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
125 * Adds a Fisher-Yates shuffle algorithm for arrays.
127 * @see http://stackoverflow.com/a/2450976
129 function shuffle(array) {
130 var currentIndex = array.length, temporaryValue, randomIndex;
132 // While there remain elements to shuffle...
133 while (0 !== currentIndex) {
134 // Pick a remaining element...
135 randomIndex = Math.floor(Math.random() * currentIndex);
138 // And swap it with the current element.
139 temporaryValue = array[currentIndex];
140 array[currentIndex] = array[randomIndex];
141 array[randomIndex] = temporaryValue;
148 * User-Agent based browser detection and touch detection.
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 ) ||
160 browser: match[ 1 ] || "",
161 version: match[ 2 ] || "0"
165 if ( matched.browser ) {
166 browser[ matched.browser ] = true;
167 browser.version = matched.version;
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;
177 jQuery.browser = browser;
178 jQuery.browser.touch = (!!('ontouchstart' in window) || (!!('msMaxTouchPoints' in window.navigator) && window.navigator.msMaxTouchPoints > 0));
180 // detect smartphones
181 jQuery.browser.smartphone = ($('html').css('caption-side') == 'bottom');
183 // CKEditor support (disabled for Android & Windows Phone)
184 jQuery.browser.ckeditor = (navigator.userAgent.match(/(Android|Windows Phone)/i)) ? false : true;
186 // properly detect IE11
187 if (jQuery.browser.mozilla && ua.match(/trident/)) {
188 jQuery.browser.mozilla = false;
189 jQuery.browser.msie = true;
194 * jQuery.browser.mobile (http://detectmobilebrowser.com/)
196 * jQuery.browser.mobile will be true if the browser is a mobile device
199 (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);
202 * Initialize WCF namespace
207 * Extends jQuery with additional methods.
211 * Removes the given value from the given array and returns the array.
214 * @param mixed element
217 removeArrayValue: function(array, value) {
218 return $.grep(array, function(element, index) {
219 return value !== element;
224 * Escapes an ID to work with jQuery selectors.
226 * @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
230 wcfEscapeID: function(id) {
231 return id.replace(/(:|\.)/g, '\\$1');
235 * Returns true if given ID exists within DOM.
240 wcfIsset: function(id) {
241 return !!$('#' + $.wcfEscapeID(id)).length;
245 * Returns the length of an object.
247 * @param object targetObject
250 getLength: function(targetObject) {
253 for (var $key in targetObject) {
254 if (targetObject.hasOwnProperty($key)) {
264 * Extends jQuery's chainable methods.
268 * Returns tag name of first jQuery element.
272 getTagName: function() {
273 return (this.length) ? this.get(0).tagName.toLowerCase() : '';
277 * Returns the dimensions for current element.
279 * @see http://api.jquery.com/hidden-selector/
283 getDimensions: function(type) {
284 var dimensions = css = {};
285 var wasHidden = false;
287 // show element to retrieve dimensions and restore them later
288 if (this.is(':hidden')) {
289 css = WCF.getInlineCSS(this);
302 height: this.innerHeight(),
303 width: this.innerWidth()
309 height: this.outerHeight(),
310 width: this.outerWidth()
316 height: this.height(),
322 // restore previous settings
324 WCF.revertInlineCSS(this, css, [ 'display', 'visibility' ]);
331 * Returns the offsets for current element, defaults to position
332 * relative to document.
334 * @see http://api.jquery.com/hidden-selector/
338 getOffsets: function(type) {
339 var offsets = css = {};
340 var wasHidden = false;
342 // show element to retrieve dimensions and restore them later
343 if (this.is(':hidden')) {
344 css = WCF.getInlineCSS(this);
355 offsets = this.offset();
360 offsets = this.position();
364 // restore previous settings
366 WCF.revertInlineCSS(this, css, [ 'display', 'visibility' ]);
373 * Changes element's position to 'absolute' or 'fixed' while maintaining it's
374 * current position relative to viewport. Optionally removes element from
375 * current DOM-node and moving it into body-element (useful for drag & drop)
377 * @param boolean rebase
380 makePositioned: function(position, rebase) {
381 if (position != 'absolute' && position != 'fixed') {
382 position = 'absolute';
385 var $currentPosition = this.getOffsets('position');
388 left: $currentPosition.left,
390 top: $currentPosition.top
394 this.remove().appentTo('body');
401 * Disables a form element.
405 disable: function() {
406 return this.attr('disabled', 'disabled');
410 * Enables a form element.
415 return this.removeAttr('disabled');
419 * Returns the element's id. If none is set, a random unique
420 * ID will be assigned.
424 wcfIdentify: function() {
425 if (!this.attr('id')) {
426 this.attr('id', WCF.getRandomID());
429 return this.attr('id');
433 * Returns the caret position of current element. If the element
434 * does not equal input[type=text], input[type=password] or
435 * textarea, -1 is returned.
439 getCaret: function() {
440 if (this.is('input')) {
441 if (this.attr('type') != 'text' && this.attr('type') != 'password') {
445 else if (!this.is('textarea')) {
450 var $element = this.get(0);
451 if (document.selection) { // IE 8
452 // set focus to enable caret on this element
455 var $selection = document.selection.createRange();
456 $selection.moveStart('character', -this.val().length);
457 $position = $selection.text.length;
459 else if ($element.selectionStart || $element.selectionStart == '0') { // Opera, Chrome, Firefox, Safari, IE 9+
460 $position = parseInt($element.selectionStart);
467 * Sets the caret position of current element. If the element
468 * does not equal input[type=text], input[type=password] or
469 * textarea, false is returned.
471 * @param integer position
474 setCaret: function (position) {
475 if (this.is('input')) {
476 if (this.attr('type') != 'text' && this.attr('type') != 'password') {
480 else if (!this.is('textarea')) {
484 var $element = this.get(0);
486 // set focus to enable caret on this element
488 if (document.selection) { // IE 8
489 var $selection = document.selection.createRange();
490 $selection.moveStart('character', position);
491 $selection.moveEnd('character', 0);
494 else if ($element.selectionStart || $element.selectionStart == '0') { // Opera, Chrome, Firefox, Safari, IE 9+
495 $element.selectionStart = position;
496 $element.selectionEnd = position;
503 * Shows an element by sliding and fading it into viewport.
505 * @param string direction
506 * @param object callback
507 * @param integer duration
510 wcfDropIn: function(direction, callback, duration) {
511 if (!direction) direction = 'up';
512 if (!duration || !parseInt(duration)) duration = 200;
514 return this.show(WCF.getEffect(this, 'drop'), { direction: direction }, duration, callback);
518 * Hides an element by sliding and fading it out the viewport.
520 * @param string direction
521 * @param object callback
522 * @param integer duration
525 wcfDropOut: function(direction, callback, duration) {
526 if (!direction) direction = 'down';
527 if (!duration || !parseInt(duration)) duration = 200;
529 return this.hide(WCF.getEffect(this, 'drop'), { direction: direction }, duration, callback);
533 * Shows an element by blinding it up.
535 * @param string direction
536 * @param object callback
537 * @param integer duration
540 wcfBlindIn: function(direction, callback, duration) {
541 if (!direction) direction = 'vertical';
542 if (!duration || !parseInt(duration)) duration = 200;
544 return this.show(WCF.getEffect(this, 'blind'), { direction: direction }, duration, callback);
548 * Hides an element by blinding it down.
550 * @param string direction
551 * @param object callback
552 * @param integer duration
555 wcfBlindOut: function(direction, callback, duration) {
556 if (!direction) direction = 'vertical';
557 if (!duration || !parseInt(duration)) duration = 200;
559 return this.hide(WCF.getEffect(this, 'blind'), { direction: direction }, duration, callback);
563 * Highlights an element.
565 * @param object options
566 * @param object callback
569 wcfHighlight: function(options, callback) {
570 return this.effect('highlight', options, 600, callback);
574 * Shows an element by fading it in.
576 * @param object callback
577 * @param integer duration
580 wcfFadeIn: function(callback, duration) {
581 if (!duration || !parseInt(duration)) duration = 200;
583 return this.show(WCF.getEffect(this, 'fade'), { }, duration, callback);
587 * Hides an element by fading it out.
589 * @param object callback
590 * @param integer duration
593 wcfFadeOut: function(callback, duration) {
594 if (!duration || !parseInt(duration)) duration = 200;
596 return this.hide(WCF.getEffect(this, 'fade'), { }, duration, callback);
601 * WoltLab Community Framework core methods
605 * count of active dialogs
611 * Counter for dynamic element ids
618 * Returns a dynamically created id.
620 * @see https://github.com/sstephenson/prototype/blob/5e5cfff7c2c253eaf415c279f9083b4650cd4506/src/prototype/dom/dom.js#L1789
623 getRandomID: function() {
627 $elementID = 'wcf' + this._idCounter++;
629 while ($.wcfIsset($elementID));
635 * Wrapper for $.inArray which returns boolean value instead of
636 * index value, similar to PHP's in_array().
638 * @param mixed needle
639 * @param array haystack
642 inArray: function(needle, haystack) {
643 return ($.inArray(needle, haystack) != -1);
647 * Adjusts effect for partially supported elements.
649 * @param jQuery object
650 * @param string effect
653 getEffect: function(object, effect) {
654 // most effects are not properly supported on table rows, use highlight instead
655 if (object.is('tr')) {
663 * Returns inline CSS for given element.
665 * @param jQuery element
668 getInlineCSS: function(element) {
669 var $inlineStyles = { };
670 var $style = element.attr('style');
672 // no style tag given or empty
677 $style = $style.split(';');
678 for (var $i = 0, $length = $style.length; $i < $length; $i++) {
679 var $fragment = $.trim($style[$i]);
680 if ($fragment == '') {
684 $fragment = $fragment.split(':');
685 $inlineStyles[$.trim($fragment[0])] = $.trim($fragment[1]);
688 return $inlineStyles;
692 * Reverts inline CSS or negates a previously set property.
694 * @param jQuery element
695 * @param object inlineCSS
696 * @param array<string> targetProperties
698 revertInlineCSS: function(element, inlineCSS, targetProperties) {
699 for (var $i = 0, $length = targetProperties.length; $i < $length; $i++) {
700 var $property = targetProperties[$i];
703 if (inlineCSS[$property]) {
704 element.css($property, inlineCSS[$property]);
708 element.css($property, '');
715 * Browser related functions.
719 * determines if browser is chrome
725 * Returns true, if browser is Chrome, Chromium or using GoogleFrame for Internet Explorer.
729 isChrome: function() {
730 if (this._isChrome === null) {
731 this._isChrome = false;
732 if (/chrom(e|ium)/.test(navigator.userAgent.toLowerCase())) {
733 this._isChrome = true;
737 return this._isChrome;
752 * initialization state
758 * list of registered dropdowns
764 * container for dropdown menus
767 _menuContainer: null,
770 * list of registered dropdown menus
776 * Initializes dropdowns.
779 if (this._menuContainer === null) {
780 this._menuContainer = $('<div id="dropdownMenuContainer
" />').appendTo(document.body);
784 $('.dropdownToggle:not(.jsDropdownEnabled)').each(function(index, button) {
785 self.initDropdown($(button), false);
788 if (!this._didInit) {
789 this._didInit = true;
791 WCF.CloseOverlayHandler.addCallback('WCF.Dropdown', $.proxy(this._closeAll, this));
792 WCF.DOMNodeInsertedHandler.addCallback('WCF.Dropdown', $.proxy(this.init, this));
793 $(document).on('scroll', $.proxy(this._scroll, this));
798 * Handles dropdown positions in overlays when scrolling in the overlay.
800 * @param object event
802 _dialogScroll: function(event) {
803 var $dialogContent = $(event.currentTarget);
804 $dialogContent.find('.dropdown.dropdownOpen').each(function(index, element) {
805 var $dropdown = $(element);
806 var $dropdownID = $dropdown.wcfIdentify();
807 var $dropdownOffset = $dropdown.offset();
808 var $dialogContentOffset = $dialogContent.offset();
810 var $verticalScrollTolerance = $(element).height() / 2;
812 // check if dropdown toggle is still (partially) visible
813 if ($dropdownOffset.top + $verticalScrollTolerance <= $dialogContentOffset.top) {
815 WCF.Dropdown.toggleDropdown($dropdownID);
817 else if ($dropdownOffset.top >= $dialogContentOffset.top + $dialogContent.height()) {
819 WCF.Dropdown.toggleDropdown($dropdownID);
821 else if ($dropdownOffset.left <= $dialogContentOffset.left) {
823 WCF.Dropdown.toggleDropdown($dropdownID);
825 else if ($dropdownOffset.left >= $dialogContentOffset.left + $dialogContent.width()) {
827 WCF.Dropdown.toggleDropdown($dropdownID);
830 WCF.Dropdown.setAlignmentByID($dropdown.wcfIdentify());
836 * Handles dropdown positions in overlays when scrolling in the document.
838 * @param object event
840 _scroll: function(event) {
841 for (var $containerID in this._dropdowns) {
842 var $dropdown = this._dropdowns[$containerID];
843 if ($dropdown.data('isOverlayDropdownButton') && $dropdown.hasClass('dropdownOpen')) {
844 this.setAlignmentByID($containerID);
850 * Initializes a dropdown.
852 * @param jQuery button
853 * @param boolean isLazyInitialization
855 initDropdown: function(button, isLazyInitialization) {
856 if (button.hasClass('jsDropdownEnabled') || button.data('target')) {
860 var $dropdown = button.parents('.dropdown');
861 if (!$dropdown.length) {
862 // broken dropdown, ignore
863 console.debug("[WCF
.Dropdown
] Invalid dropdown passed
, button
'" + button.wcfIdentify() + "' does not have a parent
with .dropdown
, aborting
.");
867 var $dropdownMenu = button.next('.dropdownMenu');
868 if (!$dropdownMenu.length) {
869 // broken dropdown, ignore
870 console.debug("[WCF
.Dropdown
] Invalid dropdown passed
, dropdown
'" + $dropdown.wcfIdentify() + "' does not have a dropdown menu
, aborting
.");
874 $dropdownMenu.detach().appendTo(this._menuContainer);
875 var $containerID = $dropdown.wcfIdentify();
876 if (!this._dropdowns[$containerID]) {
877 button.addClass('jsDropdownEnabled').click($.proxy(this._toggle, this));
879 this._dropdowns[$containerID] = $dropdown;
880 this._menus[$containerID] = $dropdownMenu;
883 button.data('target', $containerID);
885 if (isLazyInitialization) {
886 button.trigger('click');
891 * Removes the dropdown with the given container id.
893 * @param string containerID
895 removeDropdown: function(containerID) {
896 if (this._menus[containerID]) {
897 $(this._menus[containerID]).remove();
898 delete this._menus[containerID];
899 delete this._dropdowns[containerID];
904 * Initializes a dropdown fragment which behaves like a usual dropdown
905 * but is not controlled by a trigger element.
907 * @param jQuery dropdown
908 * @param jQuery dropdownMenu
910 initDropdownFragment: function(dropdown, dropdownMenu) {
911 var $containerID = dropdown.wcfIdentify();
912 if (this._dropdowns[$containerID]) {
913 console.debug("[WCF
.Dropdown
] Cannot register dropdown identified by
'" + $containerID + "' as a fragement
.");
917 this._dropdowns[$containerID] = dropdown;
918 this._menus[$containerID] = dropdownMenu.detach().appendTo(this._menuContainer);
922 * Registers a callback notified upon dropdown state change.
924 * @param string identifier
925 * @var object callback
927 registerCallback: function(identifier, callback) {
928 if (!$.isFunction(callback)) {
929 console.debug("[WCF
.Dropdown
] Callback
for '" + identifier + "' is invalid
");
933 if (!this._callbacks[identifier]) {
934 this._callbacks[identifier] = [ ];
937 this._callbacks[identifier].push(callback);
941 * Toggles a dropdown.
943 * @param object event
944 * @param string targetID
946 _toggle: function(event, targetID) {
947 var $targetID = (event === null) ? targetID : $(event.currentTarget).data('target');
949 // check if 'isOverlayDropdownButton' is set which indicates if
950 // the dropdown toggle is in an overlay
951 var $target = this._dropdowns[$targetID];
952 if ($target && $target.data('isOverlayDropdownButton') === undefined) {
953 var $dialogContent = $target.parents('.dialogContent');
954 $target.data('isOverlayDropdownButton', $dialogContent.length > 0);
956 if ($dialogContent.length) {
957 $dialogContent.on('scroll', this._dialogScroll);
961 // close all dropdowns
962 for (var $containerID in this._dropdowns) {
963 var $dropdown = this._dropdowns[$containerID];
964 var $dropdownMenu = this._menus[$containerID];
966 if ($dropdown.hasClass('dropdownOpen')) {
967 $dropdown.removeClass('dropdownOpen');
968 $dropdownMenu.removeClass('dropdownOpen');
970 this._notifyCallbacks($containerID, 'close');
972 else if ($containerID === $targetID) {
973 $dropdown.addClass('dropdownOpen');
974 $dropdownMenu.addClass('dropdownOpen');
976 this._notifyCallbacks($containerID, 'open');
978 this.setAlignment($dropdown, $dropdownMenu);
982 if (event !== null) {
983 event.stopPropagation();
989 * Toggles a dropdown.
991 * @param string containerID
993 toggleDropdown: function(containerID) {
994 this._toggle(null, containerID);
998 * Returns dropdown by container id.
1000 * @param string containerID
1003 getDropdown: function(containerID) {
1004 if (this._dropdowns[containerID]) {
1005 return this._dropdowns[containerID];
1012 * Returns dropdown menu by container id.
1014 * @param string containerID
1017 getDropdownMenu: function(containerID) {
1018 if (this._menus[containerID]) {
1019 return this._menus[containerID];
1026 * Sets alignment for given container id.
1028 * @param string containerID
1030 setAlignmentByID: function(containerID) {
1031 var $dropdown = this.getDropdown(containerID);
1032 if ($dropdown === null) {
1033 console.debug("[WCF
.Dropdown
] Unable to find dropdown identified by
'" + containerID + "'");
1036 var $dropdownMenu = this.getDropdownMenu(containerID);
1037 if ($dropdownMenu === null) {
1038 console.debug("[WCF
.Dropdown
] Unable to find dropdown menu identified by
'" + containerID + "'");
1041 this.setAlignment($dropdown, $dropdownMenu);
1045 * Sets alignment for dropdown.
1047 * @param jQuery dropdown
1048 * @param jQuery dropdownMenu
1050 setAlignment: function(dropdown, dropdownMenu) {
1051 // force dropdown menu to be placed in the upper left corner, otherwise
1052 // it might cause the calculations to be a bit off if the page exceeds
1053 // the window boundaries during getDimensions() making it visible
1054 if (!dropdownMenu.data('isInitialized')) {
1055 dropdownMenu.data('isInitialized', true).css({ left: 0, top: 0 });
1058 // get dropdown position
1059 var $dropdownDimensions = dropdown.getDimensions('outer');
1060 var $dropdownOffsets = dropdown.getOffsets('offset');
1061 var $menuDimensions = dropdownMenu.getDimensions('outer');
1062 var $windowWidth = $(window).width();
1064 // check if button belongs to an i18n textarea
1065 var $button = dropdown.find('.dropdownToggle');
1066 if ($button.hasClass('dropdownCaptionTextarea')) {
1067 // use button dimensions instead
1068 $dropdownDimensions = $button.getDimensions('outer');
1072 var $align = 'left';
1073 if (($dropdownOffsets.left + $menuDimensions.width) > $windowWidth) {
1077 // calculate offsets
1079 var $right = 'auto';
1080 if ($align === 'left') {
1081 dropdownMenu.removeClass('dropdownArrowRight');
1083 $left = $dropdownOffsets.left;
1086 dropdownMenu.addClass('dropdownArrowRight');
1088 $right = ($windowWidth - ($dropdownOffsets.left + $dropdownDimensions.width));
1091 // rtl works the same with the exception that we need to offset it with the right boundary
1092 if (WCF.Language.get('wcf.global.pageDirection') == 'rtl') {
1093 var $oldLeft = $left;
1094 var $oldRight = $right;
1096 // use reverse positioning
1097 if ($left == 'auto') {
1098 dropdownMenu.removeClass('dropdownArrowRight');
1101 $right = $windowWidth - ($dropdownOffsets.left + $dropdownDimensions.width);
1104 if ($right + $menuDimensions.width > $windowWidth) {
1105 // exceeded window width, restore ltr values
1109 dropdownMenu.addClass('dropdownArrowRight');
1114 if ($left == 'auto') $right += 'px';
1117 // calculate vertical offset
1118 var $wasHidden = true;
1119 if (dropdownMenu.hasClass('dropdownOpen')) {
1121 dropdownMenu.removeClass('dropdownOpen');
1124 var $bottom = 'auto';
1125 var $top = $dropdownOffsets.top + $dropdownDimensions.height + 7;
1126 if ($top + $menuDimensions.height > $(window).height() + $(document).scrollTop()) {
1127 $bottom = $(window).height() - $dropdownOffsets.top + 10;
1130 dropdownMenu.addClass('dropdownArrowBottom');
1133 dropdownMenu.removeClass('dropdownArrowBottom');
1137 dropdownMenu.addClass('dropdownOpen');
1149 * Closes all dropdowns.
1151 _closeAll: function() {
1152 for (var $containerID in this._dropdowns) {
1153 var $dropdown = this._dropdowns[$containerID];
1154 if ($dropdown.hasClass('dropdownOpen')) {
1155 $dropdown.removeClass('dropdownOpen');
1156 this._menus[$containerID].removeClass('dropdownOpen');
1158 this._notifyCallbacks($containerID, 'close');
1164 * Closes a dropdown without notifying callbacks.
1166 * @param string containerID
1168 close: function(containerID) {
1169 if (!this._dropdowns[containerID]) {
1173 this._dropdowns[containerID].removeClass('dropdownMenu');
1174 this._menus[containerID].removeClass('dropdownMenu');
1178 * Notifies callbacks.
1180 * @param string containerID
1181 * @param string action
1183 _notifyCallbacks: function(containerID, action) {
1184 if (!this._callbacks[containerID]) {
1188 for (var $i = 0, $length = this._callbacks[containerID].length; $i < $length; $i++) {
1189 this._callbacks[containerID][$i](containerID, action);
1199 * action proxy object
1200 * @var WCF.Action.Proxy
1211 * list of clipboard containers
1217 * container meta data
1220 _containerData: { },
1223 * user has marked items
1226 _hasMarkedItems: false,
1229 * list of ids of marked objects grouped by object type
1232 _markedObjectIDs: { },
1241 * current page's object id
1248 * @var WCF.Action.Proxy
1253 * list of elements already tracked for clipboard actions
1256 _trackedElements: { },
1259 * Initializes the clipboard API.
1261 * @param string page
1262 * @param integer hasMarkedItems
1263 * @param object actionObjects
1264 * @param integer pageObjectID
1266 init: function(page, hasMarkedItems, actionObjects, pageObjectID) {
1268 this._actionObjects = actionObjects || { };
1269 this._hasMarkedItems = (hasMarkedItems > 0);
1270 this._pageObjectID = parseInt(pageObjectID) || 0;
1272 this._actionProxy = new WCF.Action.Proxy({
1273 success: $.proxy(this._actionSuccess, this),
1274 url: 'index.php/ClipboardProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND
1277 this._proxy = new WCF.Action.Proxy({
1278 success: $.proxy(this._success, this),
1279 url: 'index.php/Clipboard/?t=' + SECURITY_TOKEN + SID_ARG_2ND
1282 // init containers first
1283 this._containers = $('.jsClipboardContainer').each($.proxy(function(index, container) {
1284 this._initContainer(container);
1287 // loads marked items
1288 if (this._hasMarkedItems && this._containers.length) {
1289 this._loadMarkedItems();
1293 WCF.DOMNodeInsertedHandler.addCallback('WCF.Clipboard', function() {
1294 self._containers = $('.jsClipboardContainer').each($.proxy(function(index, container) {
1295 self._initContainer(container);
1301 * Loads marked items on init.
1303 _loadMarkedItems: function() {
1304 new WCF.Action.Proxy({
1307 containerData: this._containerData,
1308 pageClassName: this._page,
1309 pageObjectID: this._pageObjectID
1311 success: $.proxy(this._loadMarkedItemsSuccess, this),
1312 url: 'index.php/ClipboardLoadMarkedItems/?t=' + SECURITY_TOKEN + SID_ARG_2ND
1317 * Reloads the list of marked items.
1319 reload: function() {
1320 if (this._containers === null) {
1324 this._loadMarkedItems();
1328 * Marks all returned items as marked
1330 * @param object data
1331 * @param string textStatus
1332 * @param jQuery jqXHR
1334 _loadMarkedItemsSuccess: function(data, textStatus, jqXHR) {
1335 this._resetMarkings();
1337 for (var $typeName in data.markedItems) {
1338 if (!this._markedObjectIDs[$typeName]) {
1339 this._markedObjectIDs[$typeName] = [ ];
1342 var $objectData = data.markedItems[$typeName];
1343 for (var $i in $objectData) {
1344 this._markedObjectIDs[$typeName].push($objectData[$i]);
1347 // loop through all containers
1348 this._containers.each($.proxy(function(index, container) {
1349 var $container = $(container);
1351 // typeName does not match, continue
1352 if ($container.data('type') != $typeName) {
1356 // mark items as marked
1357 $container.find('input.jsClipboardItem').each($.proxy(function(innerIndex, item) {
1358 var $item = $(item);
1359 if (WCF.inArray($item.data('objectID'), this._markedObjectIDs[$typeName])) {
1360 $item.prop('checked', true);
1362 // add marked class for element container
1363 $item.parents('.jsClipboardObject').addClass('jsMarked');
1367 // check if there is a markAll-checkbox
1368 $container.find('input.jsClipboardMarkAll').each(function(innerIndex, markAll) {
1369 var $allItemsMarked = true;
1371 $container.find('input.jsClipboardItem').each(function(itemIndex, item) {
1372 var $item = $(item);
1373 if (!$item.prop('checked')) {
1374 $allItemsMarked = false;
1378 if ($allItemsMarked) {
1379 $(markAll).prop('checked', true);
1385 // call success method to build item list editors
1386 this._success(data, textStatus, jqXHR);
1390 * Resets all checkboxes.
1392 _resetMarkings: function() {
1393 this._containers.each($.proxy(function(index, container) {
1394 var $container = $(container);
1396 this._markedObjectIDs[$container.data('type')] = [ ];
1397 $container.find('input.jsClipboardItem, input.jsClipboardMarkAll').prop('checked', false);
1398 $container.find('.jsClipboardObject').removeClass('jsMarked');
1403 * Initializes a clipboard container.
1405 * @param object container
1407 _initContainer: function(container) {
1408 var $container = $(container);
1409 var $containerID = $container.wcfIdentify();
1411 if (!this._trackedElements[$containerID]) {
1412 $container.find('.jsClipboardMarkAll').data('hasContainer', $containerID).click($.proxy(this._markAll, this));
1414 this._markedObjectIDs[$container.data('type')] = [ ];
1415 this._containerData[$container.data('type')] = {};
1416 $.each($container.data(), $.proxy(function(index, element) {
1417 if (index.match(/^type(.+)/)) {
1418 this._containerData[$container.data('type')][WCF.String.lcfirst(index.replace(/^type/, ''))] = element;
1422 this._trackedElements[$containerID] = [ ];
1425 // track individual checkboxes
1426 $container.find('input.jsClipboardItem').each($.proxy(function(index, input) {
1427 var $input = $(input);
1428 var $inputID = $input.wcfIdentify();
1430 if (!WCF.inArray($inputID, this._trackedElements[$containerID])) {
1431 this._trackedElements[$containerID].push($inputID);
1433 $input.data('hasContainer', $containerID).click($.proxy(this._click, this));
1439 * Processes change checkbox state.
1441 * @param object event
1443 _click: function(event) {
1444 var $item = $(event.target);
1445 var $objectID = $item.data('objectID');
1446 var $isMarked = ($item.prop('checked')) ? true : false;
1447 var $objectIDs = [ $objectID ];
1449 if ($item.data('hasContainer')) {
1450 var $container = $('#' + $item.data('hasContainer'));
1451 var $type = $container.data('type');
1454 var $type = $item.data('type');
1458 this._markedObjectIDs[$type].push($objectID);
1459 $item.parents('.jsClipboardObject').addClass('jsMarked');
1462 this._markedObjectIDs[$type] = $.removeArrayValue(this._markedObjectIDs[$type], $objectID);
1463 $item.parents('.jsClipboardObject').removeClass('jsMarked');
1466 // item is part of a container
1467 if ($item.data('hasContainer')) {
1468 // check if all items are marked
1469 var $markedAll = true;
1470 $container.find('input.jsClipboardItem').each(function(index, containerItem) {
1471 var $containerItem = $(containerItem);
1472 if (!$containerItem.prop('checked')) {
1477 // simulate a ticked 'markAll' checkbox
1478 $container.find('.jsClipboardMarkAll').each(function(index, markAll) {
1480 $(markAll).prop('checked', true);
1483 $(markAll).prop('checked', false);
1488 this._saveState($type, $objectIDs, $isMarked);
1492 * Marks all associated clipboard items as checked.
1494 * @param object event
1496 _markAll: function(event) {
1497 var $item = $(event.target);
1498 var $objectIDs = [ ];
1499 var $isMarked = true;
1501 // if markAll object is a checkbox, allow toggling
1502 if ($item.is('input')) {
1503 $isMarked = $item.prop('checked');
1506 if ($item.data('hasContainer')) {
1507 var $container = $('#' + $item.data('hasContainer'));
1508 var $type = $container.data('type');
1511 var $type = $item.data('type');
1514 // handle item containers
1515 if ($item.data('hasContainer')) {
1516 // toggle state for all associated items
1517 $container.find('input.jsClipboardItem').each($.proxy(function(index, containerItem) {
1518 var $containerItem = $(containerItem);
1519 var $objectID = $containerItem.data('objectID');
1521 if (!$containerItem.prop('checked')) {
1522 $containerItem.prop('checked', true);
1523 this._markedObjectIDs[$type].push($objectID);
1524 $objectIDs.push($objectID);
1528 if ($containerItem.prop('checked')) {
1529 $containerItem.prop('checked', false);
1530 this._markedObjectIDs[$type] = $.removeArrayValue(this._markedObjectIDs[$type], $objectID);
1531 $objectIDs.push($objectID);
1537 $container.find('.jsClipboardObject').addClass('jsMarked');
1540 $container.find('.jsClipboardObject').removeClass('jsMarked');
1545 this._saveState($type, $objectIDs, $isMarked);
1549 * Saves clipboard item state.
1551 * @param string type
1552 * @param array objectIDs
1553 * @param boolean isMarked
1555 _saveState: function(type, objectIDs, isMarked) {
1556 this._proxy.setOption('data', {
1557 action: (isMarked) ? 'mark' : 'unmark',
1558 containerData: this._containerData,
1559 objectIDs: objectIDs,
1560 pageClassName: this._page,
1561 pageObjectID: this._pageObjectID,
1564 this._proxy.sendRequest();
1568 * Updates editor options.
1570 * @param object data
1571 * @param string textStatus
1572 * @param jQuery jqXHR
1574 _success: function(data, textStatus, jqXHR) {
1575 // clear all editors first
1576 var $containers = {};
1577 $('.jsClipboardEditor').each(function(index, container) {
1578 var $container = $(container);
1579 var $types = eval($container.data('types'));
1580 for (var $i = 0, $length = $types.length; $i < $length; $i++) {
1581 var $typeName = $types[$i];
1582 $containers[$typeName] = $container;
1585 var $containerID = $container.wcfIdentify();
1586 WCF.CloseOverlayHandler.removeCallback($containerID);
1591 // do not build new editors
1592 if (!data.items) return;
1595 for (var $typeName in data.items) {
1596 if (!$containers[$typeName]) {
1601 var $container = $containers[$typeName];
1602 var $list = $container.children('ul');
1603 if ($list.length == 0) {
1604 $list = $('<ul />').appendTo($container);
1607 var $editor = data.items[$typeName];
1608 var $label = $('<li class="dropdown
"><span class="dropdownToggle button
">' + $editor.label + '</span></li>').appendTo($list);
1609 var $itemList = $('<ol class="dropdownMenu
"></ol>').appendTo($label);
1611 // create editor items
1612 for (var $itemIndex in $editor.items) {
1613 var $item = $editor.items[$itemIndex];
1615 var $listItem = $('<li><span>' + $item.label + '</span></li>').appendTo($itemList);
1616 $listItem.data('container', $container);
1617 $listItem.data('objectType', $typeName);
1618 $listItem.data('actionName', $item.actionName).data('parameters', $item.parameters);
1619 $listItem.data('internalData', $item.internalData).data('url', $item.url).data('type', $typeName);
1622 $listItem.click($.proxy(this._executeAction, this));
1626 $('<li class="dropdownDivider
" />').appendTo($itemList);
1627 var $foo = $typeName;
1628 $('<li><span>' + WCF.Language.get('wcf.clipboard.item.unmarkAll') + '</span></li>').data('typeName', $typeName).appendTo($itemList).click($.proxy(function(event) {
1629 var $typeName = $(event.currentTarget).data('typeName');
1631 this._proxy.setOption('data', {
1632 action: 'unmarkAll',
1635 this._proxy.setOption('success', $.proxy(function(data, textStatus, jqXHR) {
1636 this._containers.each($.proxy(function(index, container) {
1637 var $container = $(container);
1638 if ($container.data('type') == $typeName) {
1639 $container.find('.jsClipboardMarkAll, .jsClipboardItem').prop('checked', false);
1640 $container.find('.jsClipboardObject').removeClass('jsMarked');
1646 // call and restore success method
1647 this._success(data, textStatus, jqXHR);
1648 this._proxy.setOption('success', $.proxy(this._success, this));
1649 this._loadMarkedItems();
1651 this._proxy.sendRequest();
1654 WCF.Dropdown.initDropdown($label.children('.dropdownToggle'), false);
1659 * Closes the clipboard editor item list.
1661 _closeLists: function() {
1662 $('.jsClipboardEditor ul').removeClass('dropdownOpen');
1666 * Executes a clipboard editor item action.
1668 * @param object event
1670 _executeAction: function(event) {
1671 var $listItem = $(event.currentTarget);
1672 var $url = $listItem.data('url');
1674 window.location.href = $url;
1677 if ($listItem.data('parameters').className && $listItem.data('parameters').actionName) {
1678 if ($listItem.data('parameters').actionName === 'unmarkAll' || $listItem.data('parameters').objectIDs) {
1679 var $confirmMessage = $listItem.data('internalData')['confirmMessage'];
1680 if ($confirmMessage) {
1681 var $template = $listItem.data('internalData')['template'];
1682 if ($template) $template = $($template);
1684 WCF.System.Confirmation.show($confirmMessage, $.proxy(function(action) {
1685 if (action === 'confirm') {
1688 if ($template && $template.length) {
1689 $('#wcfSystemConfirmationContent').find('input, select, textarea').each(function(index, item) {
1690 var $item = $(item);
1691 $data[$item.prop('name')] = $item.val();
1695 this._executeAJAXActions($listItem, $data);
1697 }, this), '', $template);
1700 this._executeAJAXActions($listItem, { });
1706 $listItem.data('container').trigger('clipboardAction', [ $listItem.data('type'), $listItem.data('actionName'), $listItem.data('parameters') ]);
1710 * Executes the AJAX actions for the given editor list item.
1712 * @param jQuery listItem
1713 * @param object data
1715 _executeAJAXActions: function(listItem, data) {
1717 var $objectIDs = [];
1718 if (listItem.data('parameters').actionName !== 'unmarkAll') {
1719 $.each(listItem.data('parameters').objectIDs, function(index, objectID) {
1720 $objectIDs.push(parseInt(objectID));
1726 containerData: this._containerData[listItem.data('type')]
1728 var $__parameters = listItem.data('internalData')['parameters'];
1729 if ($__parameters !== undefined) {
1730 for (var $key in $__parameters) {
1731 $parameters[$key] = $__parameters[$key];
1735 new WCF.Action.Proxy({
1738 actionName: listItem.data('parameters').actionName,
1739 className: listItem.data('parameters').className,
1740 objectIDs: $objectIDs,
1741 parameters: $parameters
1743 success: $.proxy(function(data) {
1744 if (listItem.data('parameters').actionName !== 'unmarkAll') {
1745 listItem.data('container').trigger('clipboardActionResponse', [ data, listItem.data('type'), listItem.data('actionName'), listItem.data('parameters') ]);
1748 this._loadMarkedItems();
1752 if (this._actionObjects[listItem.data('objectType')] && this._actionObjects[listItem.data('objectType')][listItem.data('parameters').actionName]) {
1753 this._actionObjects[listItem.data('objectType')][listItem.data('parameters').actionName].triggerEffect($objectIDs);
1758 * Sends a clipboard proxy request.
1760 * @param object item
1762 sendRequest: function(item) {
1763 var $item = $(item);
1765 this._actionProxy.setOption('data', {
1766 parameters: $item.data('parameters'),
1767 typeName: $item.data('type')
1769 this._actionProxy.sendRequest();
1774 * Provides a simple call for periodical executed functions. Based upon
1775 * ideas by Prototype's PeriodicalExecuter.
1777 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/periodical_executer.js
1778 * @param function callback
1779 * @param integer delay
1781 WCF.PeriodicalExecuter = Class.extend({
1783 * callback for each execution cycle
1804 _isExecuting: false,
1807 * Initializes a periodical executer.
1809 * @param function callback
1810 * @param integer delay
1812 init: function(callback, delay) {
1813 if (!$.isFunction(callback)) {
1814 console.debug('[WCF.PeriodicalExecuter] Given callback is invalid, aborting.');
1818 this._callback = callback;
1819 this._interval = delay;
1824 * Executes callback.
1826 _execute: function() {
1827 if (!this._isExecuting) {
1829 this._isExecuting = true;
1830 this._callback(this);
1831 this._isExecuting = false;
1834 this._isExecuting = false;
1844 if (!this._intervalID) {
1848 clearInterval(this._intervalID);
1852 * Resumes the interval-based callback execution.
1854 resume: function() {
1855 if (this._intervalID) {
1859 this._intervalID = setInterval($.proxy(this._execute, this), this._interval);
1864 * Handler for loading overlays
1866 WCF.LoadingOverlayHandler = {
1868 * count of active loading-requests
1877 _loadingOverlay: null,
1880 * WCF.PeriodicalExecuter instance
1881 * @var WCF.PeriodicalExecuter
1886 * Adds one loading-request and shows the loading overlay if nessercery
1889 if (this._loadingOverlay === null) { // create loading overlay on first run
1890 this._loadingOverlay = $('<div class="spinner
"><span class="icon icon48 icon
-spinner
" /> <span>' + WCF.Language.get('wcf.global.loading') + '</span></div>').appendTo($('body'));
1893 var $width = this._loadingOverlay.outerWidth();
1894 if ($width < 70) $width = 70;
1895 this._loadingOverlay.css({
1896 marginLeft: Math.ceil(-1 * $width / 2),
1901 this._activeRequests++;
1902 if (this._activeRequests == 1) {
1903 if (this._pending === null) {
1905 this._pending = new WCF.PeriodicalExecuter(function(pe) {
1906 if (self._activeRequests) {
1907 self._loadingOverlay.stop(true, true).fadeIn(100);
1911 self._pending = null;
1919 * Removes one loading-request and hides loading overlay if there're no more pending requests
1922 this._activeRequests--;
1923 if (this._activeRequests == 0) {
1924 if (this._pending !== null) {
1925 this._pending.stop();
1926 this._pending = null;
1929 this._loadingOverlay.stop(true, true).fadeOut(100);
1934 * Updates a icon to/from spinner
1936 * @param jQuery target
1937 * @pram boolean loading
1939 updateIcon: function(target, loading) {
1940 var $method = (loading === undefined || loading ? 'addClass' : 'removeClass');
1942 target.find('.icon')[$method]('icon-spinner');
1943 if (target.hasClass('icon')) {
1944 target[$method]('icon-spinner');
1950 * Namespace for AJAXProxies
1955 * Basic implementation for AJAX-based proxyies
1957 * @param object options
1959 WCF.Action.Proxy = Class.extend({
1961 * shows loading overlay for a single request
1964 _showLoadingOverlayOnce: false,
1970 _suppressErrors: false,
1979 * Initializes AJAXProxy.
1981 * @param object options
1983 init: function(options) {
1984 // initialize default values
1985 this.options = $.extend(true, {
1994 showLoadingOverlay: true,
1996 suppressErrors: false,
1998 url: 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND,
2000 autoAbortPrevious: false
2003 this.confirmationDialog = null;
2004 this.loading = null;
2005 this._showLoadingOverlayOnce = false;
2006 this._suppressErrors = (this.options.suppressErrors === true);
2008 // send request immediately after initialization
2009 if (this.options.autoSend) {
2014 $(window).on('beforeunload', function() { self._suppressErrors = true; });
2018 * Sends an AJAX request.
2020 * @param abortPrevious boolean
2023 sendRequest: function(abortPrevious) {
2026 if (abortPrevious || this.options.autoAbortPrevious) {
2027 this.abortPrevious();
2030 this._lastRequest = $.ajax({
2031 data: this.options.data,
2032 dataType: this.options.dataType,
2033 jsonp: this.options.jsonp,
2034 async: this.options.async,
2035 type: this.options.type,
2036 url: this.options.url,
2037 success: $.proxy(this._success, this),
2038 error: $.proxy(this._failure, this)
2040 return this._lastRequest;
2044 * Aborts the previous request
2046 abortPrevious: function() {
2047 if (this._lastRequest !== null) {
2048 this._lastRequest.abort();
2049 this._lastRequest = null;
2054 * Shows loading overlay for a single request.
2056 showLoadingOverlayOnce: function() {
2057 this._showLoadingOverlayOnce = true;
2061 * Suppressed errors for this action proxy.
2063 suppressErrors: function() {
2064 this._suppressErrors = true;
2068 * Fires before request is send, displays global loading status.
2071 if ($.isFunction(this.options.init)) {
2072 this.options.init(this);
2075 if (this.options.showLoadingOverlay || this._showLoadingOverlayOnce) {
2076 WCF.LoadingOverlayHandler.show();
2081 * Handles AJAX errors.
2083 * @param object jqXHR
2084 * @param string textStatus
2085 * @param string errorThrown
2087 _failure: function(jqXHR, textStatus, errorThrown) {
2088 if (textStatus == 'abort') {
2089 // call child method if applicable
2090 if ($.isFunction(this.options.aborted)) {
2091 this.options.aborted(jqXHR);
2098 var $data = $.parseJSON(jqXHR.responseText);
2100 // call child method if applicable
2101 var $showError = true;
2102 if ($.isFunction(this.options.failure)) {
2103 $showError = this.options.failure($data, jqXHR, textStatus, errorThrown);
2106 if (!this._suppressErrors && $showError !== false) {
2108 if ($data.stacktrace) $details = '<br /><p>Stacktrace:</p><p>' + $data.stacktrace + '</p>';
2109 else if ($data.exceptionID) $details = '<br /><p>Exception ID: <code>' + $data.exceptionID + '</code></p>';
2111 $('<div class="ajaxDebugMessage
"><p>' + $data.message + '</p>' + $details + '</div>').wcfDialog({ title: WCF.Language.get('wcf.global.error.title') });
2114 // failed to parse JSON
2116 // call child method if applicable
2117 var $showError = true;
2118 if ($.isFunction(this.options.failure)) {
2119 $showError = this.options.failure(null, jqXHR, textStatus, errorThrown);
2122 if (!this._suppressErrors && $showError !== false) {
2123 var $message = (textStatus === 'timeout') ? WCF.Language.get('wcf.global.error.timeout') : jqXHR.responseText;
2125 // validate if $message is neither empty nor 'undefined'
2126 if ($message && $message != 'undefined') {
2127 $('<div class="ajaxDebugMessage
"><p>' + $message + '</p></div>').wcfDialog({ title: WCF.Language.get('wcf.global.error.title') });
2136 * Handles successful AJAX requests.
2138 * @param object data
2139 * @param string textStatus
2140 * @param object jqXHR
2142 _success: function(data, textStatus, jqXHR) {
2143 // call child method if applicable
2144 if ($.isFunction(this.options.success)) {
2145 // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring
2146 if (data && data.returnValues && data.returnValues.template !== undefined) {
2147 data.returnValues.template = $.trim(data.returnValues.template);
2150 this.options.success(data, textStatus, jqXHR);
2157 * Fires after an AJAX request, hides global loading status.
2159 _after: function() {
2160 this._lastRequest = null;
2161 if ($.isFunction(this.options.after)) {
2162 this.options.after();
2165 if (this.options.showLoadingOverlay || this._showLoadingOverlayOnce) {
2166 WCF.LoadingOverlayHandler.hide();
2168 if (this._showLoadingOverlayOnce) {
2169 this._showLoadingOverlayOnce = false;
2173 WCF.DOMNodeInsertedHandler.execute();
2175 // fix anchor tags generated through WCF::getAnchor()
2176 $('a[href*=#]').each(function(index, link) {
2177 var $link = $(link);
2178 if ($link.prop('href').indexOf('AJAXProxy') != -1) {
2179 var $anchor = $link.prop('href').substr($link.prop('href').indexOf('#'));
2180 var $pageLink = document.location.toString().replace(/#.*/, '');
2181 $link.prop('href', $pageLink + $anchor);
2187 * Sets options, MUST be used to set parameters before sending request
2188 * if calling from child classes.
2190 * @param string optionName
2191 * @param mixed optionData
2193 setOption: function(optionName, optionData) {
2194 this.options[optionName] = optionData;
2199 * Basic implementation for simple proxy access using bound elements.
2201 * @param object options
2202 * @param object callbacks
2204 WCF.Action.SimpleProxy = Class.extend({
2206 * Initializes SimpleProxy.
2208 * @param object options
2209 * @param object callbacks
2211 init: function(options, callbacks) {
2213 * action-specific options
2215 this.options = $.extend(true, {
2223 * proxy-specific options
2225 this.callbacks = $.extend(true, {
2232 if (!this.options.elements) return;
2235 this.proxy = new WCF.Action.Proxy(this.callbacks);
2237 // bind event listener
2238 this.options.elements.each($.proxy(function(index, element) {
2239 $(element).bind(this.options.eventName, $.proxy(this._handleEvent, this));
2244 * Handles event actions.
2246 * @param object event
2248 _handleEvent: function(event) {
2249 this.proxy.setOption('data', {
2250 actionName: this.options.action,
2251 className: this.options.className,
2252 objectIDs: [ $(event.target).data('objectID') ]
2255 this.proxy.sendRequest();
2260 * Basic implementation for AJAXProxy-based deletion.
2262 * @param string className
2263 * @param string containerSelector
2264 * @param string buttonSelector
2266 WCF.Action.Delete = Class.extend({
2268 * delete button selector
2271 _buttonSelector: '',
2280 * container selector
2283 _containerSelector: '',
2286 * list of known container ids
2287 * @var array<string>
2292 * Initializes 'delete'-Proxy.
2294 * @param string className
2295 * @param string containerSelector
2296 * @param string buttonSelector
2298 init: function(className, containerSelector, buttonSelector) {
2299 this._containerSelector = containerSelector;
2300 this._className = className;
2301 this._buttonSelector = (buttonSelector) ? buttonSelector : '.jsDeleteButton';
2303 this.proxy = new WCF.Action.Proxy({
2304 success: $.proxy(this._success, this)
2307 this._initElements();
2309 WCF.DOMNodeInsertedHandler.addCallback('WCF.Action.Delete' + this._className.hashCode(), $.proxy(this._initElements, this));
2313 * Initializes available element containers.
2315 _initElements: function() {
2317 $(this._containerSelector).each(function(index, container) {
2318 var $container = $(container);
2319 var $containerID = $container.wcfIdentify();
2321 if (!WCF.inArray($containerID, self._containers)) {
2322 self._containers.push($containerID);
2323 $container.find(self._buttonSelector).click($.proxy(self._click, self));
2329 * Sends AJAX request.
2331 * @param object event
2333 _click: function(event) {
2334 var $target = $(event.currentTarget);
2335 event.preventDefault();
2337 if ($target.data('confirmMessage')) {
2338 WCF.System.Confirmation.show($target.data('confirmMessage'), $.proxy(this._execute, this), { target: $target });
2341 WCF.LoadingOverlayHandler.updateIcon($target);
2342 this._sendRequest($target);
2347 * Is called if the delete effect has been triggered on the given element.
2349 * @param jQuery element
2351 _didTriggerEffect: function(element) {
2356 * Executes deletion.
2358 * @param string action
2359 * @param object parameters
2361 _execute: function(action, parameters) {
2362 if (action === 'cancel') {
2366 WCF.LoadingOverlayHandler.updateIcon(parameters.target);
2367 this._sendRequest(parameters.target);
2373 * @param jQuery object
2375 _sendRequest: function(object) {
2376 this.proxy.setOption('data', {
2377 actionName: 'delete',
2378 className: this._className,
2379 interfaceName: 'wcf\\data\\IDeleteAction',
2380 objectIDs: [ $(object).data('objectID') ]
2383 this.proxy.sendRequest();
2387 * Deletes items from containers.
2389 * @param object data
2390 * @param string textStatus
2391 * @param object jqXHR
2393 _success: function(data, textStatus, jqXHR) {
2394 this.triggerEffect(data.objectIDs);
2398 * Triggers the delete effect for the objects with the given ids.
2400 * @param array objectIDs
2402 triggerEffect: function(objectIDs) {
2403 for (var $index in this._containers) {
2404 var $container = $('#' + this._containers[$index]);
2405 if (WCF.inArray($container.find(this._buttonSelector).data('objectID'), objectIDs)) {
2407 $container.wcfBlindOut('up',function() {
2409 self._containers.splice(self._containers.indexOf($(this).wcfIdentify()), 1);
2410 self._didTriggerEffect($(this));
2418 * Basic implementation for deletion of nested elements.
2420 * The implementation requires the nested elements to be grouped as numbered lists
2421 * (ol lists). The child elements of the deleted elements are moved to the parent
2422 * element of the deleted element.
2424 * @see WCF.Action.Delete
2426 WCF.Action.NestedDelete = WCF.Action.Delete.extend({
2428 * @see WCF.Action.Delete.triggerEffect()
2430 triggerEffect: function(objectIDs) {
2431 for (var $index in this._containers) {
2432 var $container = $('#' + this._containers[$index]);
2433 if (WCF.inArray($container.find(this._buttonSelector).data('objectID'), objectIDs)) {
2435 if ($container.has('ol').has('li').length) {
2436 if ($container.is(':only-child')) {
2437 $container.parent().replaceWith($container.find('> ol'));
2440 $container.replaceWith($container.find('> ol > li'));
2443 this._containers.splice(this._containers.indexOf($container.wcfIdentify()), 1);
2444 this._didTriggerEffect($container);
2448 $container.wcfBlindOut('up', function() {
2450 self._containers.splice(self._containers.indexOf($(this).wcfIdentify()), 1);
2451 self._didTriggerEffect($(this));
2460 * Basic implementation for AJAXProxy-based toggle actions.
2462 * @param string className
2463 * @param jQuery containerList
2464 * @param string buttonSelector
2466 WCF.Action.Toggle = Class.extend({
2468 * toogle button selector
2471 _buttonSelector: '.jsToggleButton',
2480 * container selector
2483 _containerSelector: '',
2486 * list of known container ids
2487 * @var array<string>
2492 * Initializes 'toggle'-Proxy
2494 * @param string className
2495 * @param string containerSelector
2496 * @param string buttonSelector
2498 init: function(className, containerSelector, buttonSelector) {
2499 this._containerSelector = containerSelector;
2500 this._className = className;
2501 this._buttonSelector = (buttonSelector) ? buttonSelector : '.jsToggleButton';
2502 this._containers = [ ];
2506 success: $.proxy(this._success, this)
2508 this.proxy = new WCF.Action.Proxy(options);
2510 // bind event listener
2511 this._initElements();
2512 WCF.DOMNodeInsertedHandler.addCallback('WCF.Action.Toggle' + this._className.hashCode(), $.proxy(this._initElements, this));
2516 * Initializes available element containers.
2518 _initElements: function() {
2519 $(this._containerSelector).each($.proxy(function(index, container) {
2520 var $container = $(container);
2521 var $containerID = $container.wcfIdentify();
2523 if (!WCF.inArray($containerID, this._containers)) {
2524 this._containers.push($containerID);
2525 $container.find(this._buttonSelector).click($.proxy(this._click, this));
2531 * Sends AJAX request.
2533 * @param object event
2535 _click: function(event) {
2536 var $target = $(event.currentTarget);
2537 event.preventDefault();
2539 if ($target.data('confirmMessage')) {
2540 WCF.System.Confirmation.show($target.data('confirmMessage'), $.proxy(this._execute, this), { target: $target });
2543 WCF.LoadingOverlayHandler.updateIcon($target);
2544 this._sendRequest($target);
2549 * Executes toggeling.
2551 * @param string action
2552 * @param object parameters
2554 _execute: function(action, parameters) {
2555 if (action === 'cancel') {
2559 WCF.LoadingOverlayHandler.updateIcon(parameters.target);
2560 this._sendRequest(parameters.target);
2563 _sendRequest: function(object) {
2564 this.proxy.setOption('data', {
2565 actionName: 'toggle',
2566 className: this._className,
2567 interfaceName: 'wcf\\data\\IToggleAction',
2568 objectIDs: [ $(object).data('objectID') ]
2571 this.proxy.sendRequest();
2575 * Toggles status icons.
2577 * @param object data
2578 * @param string textStatus
2579 * @param object jqXHR
2581 _success: function(data, textStatus, jqXHR) {
2582 this.triggerEffect(data.objectIDs);
2586 * Triggers the toggle effect for the objects with the given ids.
2588 * @param array objectIDs
2590 triggerEffect: function(objectIDs) {
2591 for (var $index in this._containers) {
2592 var $container = $('#' + this._containers[$index]);
2593 var $toggleButton = $container.find(this._buttonSelector);
2594 if (WCF.inArray($toggleButton.data('objectID'), objectIDs)) {
2595 $container.wcfHighlight();
2596 this._toggleButton($container, $toggleButton);
2602 * Tiggers the toggle effect on a button
2604 * @param jQuery $container
2605 * @param jQuery $toggleButton
2607 _toggleButton: function($container, $toggleButton) {
2608 // toggle icon source
2609 WCF.LoadingOverlayHandler.updateIcon($toggleButton, false);
2610 if ($toggleButton.hasClass('icon-check-empty')) {
2611 $toggleButton.removeClass('icon-check-empty').addClass('icon-check');
2612 $newTitle = ($toggleButton.data('disableTitle') ? $toggleButton.data('disableTitle') : WCF.Language.get('wcf.global.button.disable'));
2613 $toggleButton.attr('title', $newTitle);
2616 $toggleButton.removeClass('icon-check').addClass('icon-check-empty');
2617 $newTitle = ($toggleButton.data('enableTitle') ? $toggleButton.data('enableTitle') : WCF.Language.get('wcf.global.button.enable'));
2618 $toggleButton.attr('title', $newTitle);
2622 $container.toggleClass('disabled');
2627 * Executes provided callback if scroll threshold is reached. Usuable to determine
2628 * if user reached the bottom of an element to load new elements on the fly.
2630 * If you do not provide a value for 'reference' and 'target' it will assume you're
2631 * monitoring page scrolls, otherwise a valid jQuery selector must be provided for both.
2633 * @param integer threshold
2634 * @param object callback
2635 * @param string reference
2636 * @param string target
2638 WCF.Action.Scroll = Class.extend({
2640 * callback used once threshold is reached
2664 * Initializes a new WCF.Action.Scroll object.
2666 * @param integer threshold
2667 * @param object callback
2668 * @param string reference
2669 * @param string target
2671 init: function(threshold, callback, reference, target) {
2672 this._threshold = parseInt(threshold);
2673 if (this._threshold === 0) {
2674 console.debug("[WCF
.Action
.Scroll
] Given threshold is invalid
, aborting
.");
2678 if ($.isFunction(callback)) this._callback = callback;
2679 if (this._callback === null) {
2680 console.debug("[WCF
.Action
.Scroll
] Given callback is invalid
, aborting
.");
2684 // bind element references
2685 this._reference = $((reference) ? reference : window);
2686 this._target = $((target) ? target : document);
2688 // watch for scroll event
2691 // check if browser navigated back and jumped to offset before JavaScript was loaded
2696 * Calculates if threshold is reached and notifies callback.
2698 _scroll: function() {
2699 var $targetHeight = this._target.height();
2700 var $topOffset = this._reference.scrollTop();
2701 var $referenceHeight = this._reference.height();
2703 // calculate if defined threshold is visible
2704 if (($targetHeight - ($referenceHeight + $topOffset)) < this._threshold) {
2705 this._callback(this);
2710 * Enables scroll monitoring, may be used to resume.
2713 this._reference.on('scroll', $.proxy(this._scroll, this));
2717 * Disables scroll monitoring, e.g. no more elements loadable.
2720 this._reference.off('scroll');
2725 * Namespace for date-related functions.
2730 * Provides a date picker for date input fields.
2737 _dateFormat: 'yy-mm-dd',
2743 _timeFormat: 'g:ia',
2746 * Initializes the jQuery UI based date picker.
2749 // ignore error 'unexpected literal' error; this might be not the best approach
2750 // to fix this problem, but since the date is properly processed anyway, we can
2751 // simply continue :) - Alex
2752 var $__log = $.timepicker.log;
2753 $.timepicker.log = function(error) {
2754 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) {
2759 this._convertDateFormat();
2760 this._initDatePicker();
2761 WCF.DOMNodeInsertedHandler.addCallback('WCF.Date.Picker', $.proxy(this._initDatePicker, this));
2765 * Convert PHPs date() format to jQuery UIs date picker format.
2767 _convertDateFormat: function() {
2768 // replacement table
2769 // format of PHP date() => format of jQuery UI date picker
2771 // No equivalence in PHP date():
2772 // oo day of the year (three digit)
2773 // ! Windows ticks (100ns since 01/01/0001)
2775 // No equivalence in jQuery UI date picker:
2776 // N ISO-8601 numeric representation of the day of the week
2777 // w Numeric representation of the day of the week
2778 // W ISO-8601 week number of year, weeks starting on Monday
2779 // t Number of days in the given month
2780 // L Whether it's a leap year
2781 var $replacementTable = {
2799 'S': '', // English ordinal suffix for the day of the month, 2 characters, will be discarded
2816 // do the actual replacement
2817 // this is not perfect, but a basic implementation and should work in 99% of the cases
2818 this._dateFormat = WCF.Language.get('wcf.date.dateFormat').replace(/([^dDjlzSFmMnoYyU\\]*(?:\\.[^dDjlzSFmMnoYyU\\]*)*)([dDjlzSFmMnoYyU])/g, function(match, part1, part2, offset, string) {
2819 for (var $key in $replacementTable) {
2820 if (part2 == $key) {
2821 part2 = $replacementTable[$key];
2825 return part1 + part2;
2828 this._timeFormat = WCF.Language.get('wcf.date.timeFormat').replace(/([^aAgGhHisu\\]*(?:\\.[^aAgGhHisu\\]*)*)([aAgGhHisu])/g, function(match, part1, part2, offset, string) {
2829 for (var $key in $replacementTable) {
2830 if (part2 == $key) {
2831 part2 = $replacementTable[$key];
2835 return part1 + part2;
2840 * Initializes the date picker for valid fields.
2842 _initDatePicker: function() {
2843 $('input[type=date]:not(.jsDatePicker), input[type=datetime]:not(.jsDatePicker)').each($.proxy(function(index, input) {
2844 var $input = $(input);
2845 var $inputName = $input.prop('name');
2846 var $inputValue = $input.val(); // should be Y-m-d (H:i:s), must be interpretable by Date
2848 var $hasTime = $input.attr('type') == 'datetime';
2851 $input.prop('type', 'text').addClass('jsDatePicker');
2854 if ($input.data('placeholder')) $input.attr('placeholder', $input.data('placeholder'));
2856 // insert a hidden element representing the actual date
2857 $input.removeAttr('name');
2858 $input.before('<input type="hidden
" id="' + $input.wcfIdentify() + 'DatePicker
" name="' + $inputName + '" value="' + $inputValue + '" />');
2861 var $maxDate = $input.attr('max') ? new Date($input.attr('max').replace(' ', 'T')) : null;
2862 var $minDate = $input.attr('min') ? new Date($input.attr('min').replace(' ', 'T')) : null;
2866 altField: '#' + $input.wcfIdentify() + 'DatePicker',
2867 altFormat: 'yy-mm-dd', // PHPs strtotime() understands this best
2868 beforeShow: function(input, instance) {
2869 // dirty hack to force opening below the input
2870 setTimeout(function() {
2871 instance.dpDiv.position({
2881 dateFormat: this._dateFormat,
2882 dayNames: WCF.Language.get('__days'),
2883 dayNamesMin: WCF.Language.get('__daysShort'),
2884 dayNamesShort: WCF.Language.get('__daysShort'),
2885 firstDay: parseInt(WCF.Language.get('wcf.date.firstDayOfTheWeek')) || 0,
2886 isRTL: WCF.Language.get('wcf.global.pageDirection') == 'rtl',
2889 monthNames: WCF.Language.get('__months'),
2890 monthNamesShort: WCF.Language.get('__monthsShort'),
2891 showButtonPanel: false,
2892 onClose: function(dateText, datePicker) {
2893 // clear altField when datepicker is cleared
2894 if (dateText == '') {
2895 $(datePicker.settings["altField
"]).val(dateText);
2898 showOtherMonths: true,
2899 yearRange: ($input.hasClass('birthday') ? '-100:+0' : '1900:2038')
2904 if (/[0-9]{2}:[0-9]{2}:[0-9]{2}$/.test($inputValue)) {
2905 $inputValue = $inputValue.replace(/:[0-9]{2}$/, '');
2906 $input.val($inputValue);
2908 $inputValue = $inputValue.replace(' ', 'T');
2910 if ($input.data('ignoreTimezone')) {
2911 var $timezoneOffset = new Date($inputValue).getTimezoneOffset();
2912 var $timezone = ($timezoneOffset > 0) ? '-' : '+'; // -120 equals GMT+0200
2913 $timezoneOffset = Math.abs($timezoneOffset);
2914 var $hours = (Math.floor($timezoneOffset / 60)).toString();
2915 var $minutes = ($timezoneOffset % 60).toString();
2916 $timezone += ($hours.length == 2) ? $hours : '0' + $hours;
2918 $timezone += ($minutes.length == 2) ? $minutes : '0' + $minutes;
2920 $inputValue = $inputValue.replace(/[+-][0-9]{2}:[0-9]{2}$/, $timezone);
2923 $options = $.extend($options, {
2924 altFieldTimeOnly: false,
2925 altTimeFormat: 'HH:mm',
2926 controlType: 'select',
2927 hourText: WCF.Language.get('wcf.date.hour'),
2928 minuteText: WCF.Language.get('wcf.date.minute'),
2930 timeFormat: this._timeFormat,
2931 yearRange: ($input.hasClass('birthday') ? '-100:+0' : '1900:2038')
2936 $input.datetimepicker($options);
2939 $input.datepicker($options);
2942 // format default date
2945 // drop timezone for date-only input
2946 $inputValue = new Date($inputValue);
2947 $inputValue.setMinutes($inputValue.getMinutes() + $inputValue.getTimezoneOffset());
2950 $input.datepicker('setDate', $inputValue);
2953 // bug workaround: setDate creates the widget but unfortunately doesn't hide it...
2954 $input.datepicker('widget').hide();
2960 * Provides utility functions for date operations.
2964 * Returns UTC timestamp, if date is not given, current time will be used.
2969 gmdate: function(date) {
2970 var $date = (date) ? date : new Date();
2972 return Math.round(Date.UTC(
2973 $date.getUTCFullYear(),
2974 $date.getUTCMonth(),
2976 $date.getUTCHours(),
2977 $date.getUTCMinutes(),
2978 $date.getUTCSeconds()
2983 * Returns a Date object with precise offset (including timezone and local timezone).
2984 * Parameters timestamp and offset must be in miliseconds!
2986 * @param integer timestamp
2987 * @param integer offset
2990 getTimezoneDate: function(timestamp, offset) {
2991 var $date = new Date(timestamp);
2992 var $localOffset = $date.getTimezoneOffset() * 60000;
2994 return new Date((timestamp + $localOffset + offset));
2999 * Handles relative time designations.
3001 WCF.Date.Time = Class.extend({
3003 * Date of current timestamp
3009 * list of time elements
3015 * difference between server and local time
3027 * Initializes relative datetimes.
3030 this._elements = $('time.datetime');
3031 this._offset = null;
3032 this._timestamp = 0;
3034 // calculate relative datetime on init
3037 // re-calculate relative datetime every minute
3038 new WCF.PeriodicalExecuter($.proxy(this._refresh, this), 60000);
3040 // bind dom node inserted listener
3041 WCF.DOMNodeInsertedHandler.addCallback('WCF.Date.Time', $.proxy(this._domNodeInserted, this));
3045 * Updates element collection once a DOM node was inserted.
3047 _domNodeInserted: function() {
3048 this._elements = $('time.datetime');
3053 * Refreshes relative datetime for each element.
3055 _refresh: function() {
3056 this._date = new Date();
3057 this._timestamp = (this._date.getTime() - this._date.getMilliseconds()) / 1000;
3058 if (this._offset === null) {
3059 this._offset = this._timestamp - TIME_NOW;
3062 this._elements.each($.proxy(this._refreshElement, this));
3066 * Refreshes relative datetime for current element.
3068 * @param integer index
3069 * @param object element
3071 _refreshElement: function(index, element) {
3072 var $element = $(element);
3074 if (!$element.attr('title')) {
3075 $element.attr('title', $element.text());
3078 var $timestamp = $element.data('timestamp') + this._offset;
3079 var $date = $element.data('date');
3080 var $time = $element.data('time');
3081 var $offset = $element.data('offset');
3083 // skip for future dates
3084 if ($element.data('isFutureDate')) return;
3086 // timestamp is less than 60 seconds ago
3087 if ($timestamp >= this._timestamp || this._timestamp < ($timestamp + 60)) {
3088 $element.text(WCF.Language.get('wcf.date.relative.now'));
3090 // timestamp is less than 60 minutes ago (display 1 hour ago rather than 60 minutes ago)
3091 else if (this._timestamp < ($timestamp + 3540)) {
3092 var $minutes = Math.max(Math.round((this._timestamp - $timestamp) / 60), 1);
3093 $element.text(WCF.Language.get('wcf.date.relative.minutes', { minutes: $minutes }));
3095 // timestamp is less than 24 hours ago
3096 else if (this._timestamp < ($timestamp + 86400)) {
3097 var $hours = Math.round((this._timestamp - $timestamp) / 3600);
3098 $element.text(WCF.Language.get('wcf.date.relative.hours', { hours: $hours }));
3100 // timestamp is less than 6 days ago
3101 else if (this._timestamp < ($timestamp + 518400)) {
3102 var $midnight = new Date(this._date.getFullYear(), this._date.getMonth(), this._date.getDate());
3103 var $days = Math.ceil(($midnight / 1000 - $timestamp) / 86400);
3106 var $dateObj = WCF.Date.Util.getTimezoneDate(($timestamp * 1000), $offset * 1000);
3107 var $dow = $dateObj.getDay();
3108 var $day = WCF.Language.get('__days')[$dow];
3110 $element.text(WCF.Language.get('wcf.date.relative.pastDays', { days: $days, day: $day, time: $time }));
3112 // timestamp is between ~700 million years BC and last week
3114 var $string = WCF.Language.get('wcf.date.shortDateTimeFormat');
3115 $element.text($string.replace(/\%date\%/, $date).replace(/\%time\%/, $time));
3121 * Hash-like dictionary. Based upon idead from Prototype's hash
3123 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/hash.js
3125 WCF.Dictionary = Class.extend({
3133 * Initializes a new dictionary.
3136 this._variables = { };
3143 * @param mixed value
3145 add: function(key, value) {
3146 this._variables[key] = value;
3150 * Adds a traditional object to current dataset.
3152 * @param object object
3154 addObject: function(object) {
3155 for (var $key in object) {
3156 this.add($key, object[$key]);
3161 * Adds a dictionary to current dataset.
3163 * @param object dictionary
3165 addDictionary: function(dictionary) {
3166 dictionary.each($.proxy(function(pair) {
3167 this.add(pair.key, pair.value);
3172 * Retrieves the value of an entry or returns null if key is not found.
3177 get: function(key) {
3178 if (this.isset(key)) {
3179 return this._variables[key];
3186 * Returns true if given key is a valid entry.
3190 isset: function(key) {
3191 return this._variables.hasOwnProperty(key);
3199 remove: function(key) {
3200 delete this._variables[key];
3204 * Iterates through dictionary.
3207 * var $hash = new WCF.Dictionary();
3208 * $hash.add('foo', 'bar');
3209 * $hash.each(function(pair) {
3210 * // alerts: foo = bar
3211 * alert(pair.key + ' = ' + pair.value);
3214 * @param function callback
3216 each: function(callback) {
3217 if (!$.isFunction(callback)) {
3221 for (var $key in this._variables) {
3222 var $value = this._variables[$key];
3233 * Returns the amount of items.
3238 return $.getLength(this._variables);
3242 * Returns true if dictionary is empty.
3246 isEmpty: function() {
3247 return !this.count();
3252 * Global language storage.
3254 * @see WCF.Dictionary
3257 _variables: new WCF.Dictionary(),
3260 * @see WCF.Dictionary.add()
3262 add: function(key, value) {
3263 this._variables.add(key, value);
3267 * @see WCF.Dictionary.addObject()
3269 addObject: function(object) {
3270 this._variables.addObject(object);
3274 * Retrieves a variable.
3279 get: function(key, parameters) {
3280 // initialize parameters with an empty object
3281 if (parameters == null) var parameters = { };
3283 var value = this._variables.get(key);
3285 if (value === null) {
3289 else if (typeof value === 'string') {
3290 // transform strings into template and try to refetch
3291 this.add(key, new WCF.Template(value));
3292 return this.get(key, parameters);
3294 else if (typeof value.fetch === 'function') {
3295 // evaluate templates
3296 value = value.fetch(parameters);
3304 * Handles multiple language input fields.
3306 * @param string elementID
3307 * @param boolean forceSelection
3308 * @param object values
3309 * @param object availableLanguages
3311 WCF.MultipleLanguageInput = Class.extend({
3313 * list of available languages
3316 _availableLanguages: {},
3325 * initialization state
3331 * target input element
3337 * true, if data was entered after initialization
3340 _insertedDataAfterInit: false,
3343 * enables multiple language ability
3349 * enforce multiple language ability
3352 _forceSelection: false,
3355 * currently active language id
3361 * language selection list
3367 * list of language values on init
3373 * Initializes multiple language ability for given element id.
3375 * @param integer elementID
3376 * @param boolean forceSelection
3377 * @param boolean isEnabled
3378 * @param object values
3379 * @param object availableLanguages
3381 init: function(elementID, forceSelection, values, availableLanguages) {
3382 this._button = null;
3383 this._element = $('#' + $.wcfEscapeID(elementID));
3384 this._forceSelection = forceSelection;
3385 this._values = values;
3386 this._availableLanguages = availableLanguages;
3389 if ($.getLength(this._values)) {
3390 for (var $key in this._values) {
3391 this._values[$key] = WCF.String.unescapeHTML(this._values[$key]);
3395 // default to current user language
3396 this._languageID = LANGUAGE_ID;
3397 if (this._element.length == 0) {
3398 console.debug("[WCF
.MultipleLanguageInput
] element id
'" + elementID + "' is unknown
");
3402 // build selection handler
3403 var $enableOnInit = ($.getLength(this._values) > 0) ? true : false;
3404 this._insertedDataAfterInit = $enableOnInit;
3405 this._prepareElement($enableOnInit);
3407 // listen for submit event
3408 this._element.parents('form').submit($.proxy(this._submit, this));
3410 this._didInit = true;
3414 * Builds language handler.
3416 * @param boolean enableOnInit
3418 _prepareElement: function(enableOnInit) {
3419 this._element.wrap('<div class="dropdown preInput
" />');
3420 var $wrapper = this._element.parent();
3421 this._button = $('<p class="button dropdownToggle
"><span>' + WCF.Language.get('wcf.global.button.disabledI18n') + '</span></p>').prependTo($wrapper);
3424 this._list = $('<ul class="dropdownMenu
"></ul>').insertAfter(this._button);
3426 // add a special class if next item is a textarea
3427 if (this._button.nextAll('textarea').length) {
3428 this._button.addClass('dropdownCaptionTextarea');
3431 this._button.addClass('dropdownCaption');
3434 // insert available languages
3435 for (var $languageID in this._availableLanguages) {
3436 $('<li><span>' + this._availableLanguages[$languageID] + '</span></li>').data('languageID', $languageID).click($.proxy(this._changeLanguage, this)).appendTo(this._list);
3439 // disable language input
3440 if (!this._forceSelection) {
3441 $('<li class="dropdownDivider
" />').appendTo(this._list);
3442 $('<li><span>' + WCF.Language.get('wcf.global.button.disabledI18n') + '</span></li>').click($.proxy(this._disable, this)).appendTo(this._list);
3445 WCF.Dropdown.initDropdown(this._button, enableOnInit);
3447 if (enableOnInit || this._forceSelection) {
3448 this._isEnabled = true;
3450 // pre-select current language
3451 this._list.children('li').each($.proxy(function(index, listItem) {
3452 var $listItem = $(listItem);
3453 if ($listItem.data('languageID') == this._languageID) {
3454 $listItem.trigger('click');
3459 WCF.Dropdown.registerCallback($wrapper.wcfIdentify(), $.proxy(this._handleAction, this));
3463 * Handles dropdown actions.
3465 * @param string containerID
3466 * @param string action
3468 _handleAction: function(containerID, action) {
3469 if (action === 'open') {
3473 this._closeSelection();
3478 * Enables the language selection or shows the selection if already enabled.
3480 * @param object event
3482 _enable: function(event) {
3483 if (!this._isEnabled) {
3484 var $button = (this._button.is('p')) ? this._button.children('span:eq(0)') : this._button;
3485 $button.addClass('active');
3487 this._isEnabled = true;
3491 if (this._list.is(':visible')) {
3492 this._showSelection();
3497 * Shows the language selection.
3499 _showSelection: function() {
3500 if (this._isEnabled) {
3501 // display status for each language
3502 this._list.children('li').each($.proxy(function(index, listItem) {
3503 var $listItem = $(listItem);
3504 var $languageID = $listItem.data('languageID');
3507 if (this._values[$languageID] && this._values[$languageID] != '') {
3508 $listItem.removeClass('missingValue');
3511 $listItem.addClass('missingValue');
3519 * Closes the language selection.
3521 _closeSelection: function() {
3526 * Changes the currently active language.
3528 * @param object event
3530 _changeLanguage: function(event) {
3531 var $button = $(event.currentTarget);
3532 this._insertedDataAfterInit = true;
3534 // save current value
3535 if (this._didInit) {
3536 this._values[this._languageID] = this._element.val();
3540 this._languageID = $button.data('languageID');
3541 if (this._values[this._languageID]) {
3542 this._element.val(this._values[this._languageID]);
3545 this._element.val('');
3549 this._list.children('li').removeClass('active');
3550 $button.addClass('active');
3553 this._button.children('span').addClass('active').text(this._availableLanguages[this._languageID]);
3555 // close selection and set focus on input element
3556 if (this._didInit) {
3557 this._element.blur().focus();
3562 * Disables language selection for current element.
3564 * @param object event
3566 _disable: function(event) {
3567 if (event === undefined && this._insertedDataAfterInit) {
3571 if (this._forceSelection || !this._list || event === null) {
3575 // remove active marking
3576 this._button.children('span').removeClass('active').text(WCF.Language.get('wcf.global.button.disabledI18n'));
3578 // update element value
3579 if (this._values[LANGUAGE_ID]) {
3580 this._element.val(this._values[LANGUAGE_ID]);
3583 // no value for current language found, proceed with empty input
3584 this._element.val();
3588 this._list.children('li').removeClass('active');
3589 $(event.currentTarget).addClass('active');
3592 this._element.blur().focus();
3593 this._insertedDataAfterInit = false;
3594 this._isEnabled = false;
3599 * Prepares language variables on before submit.
3601 _submit: function() {
3602 // insert hidden form elements on before submit
3603 if (!this._isEnabled) {
3607 // fetch active value
3608 if (this._languageID) {
3609 this._values[this._languageID] = this._element.val();
3612 var $form = $(this._element.parents('form')[0]);
3613 var $elementID = this._element.wcfIdentify();
3615 for (var $languageID in this._availableLanguages) {
3616 if (this._values[$languageID] === undefined) {
3617 this._values[$languageID] = '';
3620 $('<input type="hidden
" name="' + $elementID + '_i18n
[' + $languageID + ']" value="' + WCF.String.escapeHTML(this._values[$languageID]) + '" />').appendTo($form);
3623 // remove name attribute to prevent conflict with i18n values
3624 this._element.removeAttr('name');
3633 * Rounds a number to a given number of decimal places. Defaults to 0.
3635 * @param number number
3636 * @param decimalPlaces number of decimal places
3639 round: function (number, decimalPlaces) {
3640 decimalPlaces = Math.pow(10, (decimalPlaces || 0));
3642 return Math.round(number * decimalPlaces) / decimalPlaces;
3651 * Adds thousands separators to a given number.
3653 * @see http://stackoverflow.com/a/6502556/782822
3654 * @param mixed number
3657 addThousandsSeparator: function(number) {
3658 return String(number).replace(/(^-?\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, '$1' + WCF.Language.get('wcf.global.thousandsSeparator'));
3662 * Escapes special HTML-characters within a string
3664 * @param string string
3667 escapeHTML: function (string) {
3668 return String(string).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g
, '>');
3672 * Escapes a String to work with RegExp.
3674 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25
3675 * @param string string
3678 escapeRegExp: function(string
) {
3679 return String(string
).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
3683 * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands-separators
3685 * @param mixed number
3688 formatNumeric: function(number
, decimalPlaces
) {
3689 number
= String(WCF
.Number
.round(number
, decimalPlaces
|| 2));
3690 number
= number
.replace('.', WCF
.Language
.get('wcf.global.decimalPoint'));
3692 number
= this.addThousandsSeparator(number
);
3693 number
= number
.replace('-', '\u2212');
3699 * Makes a string's first character lowercase
3701 * @param string string
3704 lcfirst: function(string
) {
3705 return String(string
).substring(0, 1).toLowerCase() + string
.substring(1);
3709 * Makes a string's first character uppercase
3711 * @param string string
3714 ucfirst: function(string
) {
3715 return String(string
).substring(0, 1).toUpperCase() + string
.substring(1);
3719 * Unescapes special HTML-characters within a string
3721 * @param string string
3724 unescapeHTML: function (string
) {
3725 return String(string
).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
3730 * Basic implementation for WCF TabMenus. Use the data attributes 'active' to specify the
3731 * tab which should be shown on init. Furthermore you may specify a 'store' data-attribute
3732 * which will be filled with the currently selected tab.
3736 * list of tabmenu containers
3742 * initialization state
3748 * Initializes all TabMenus
3751 var $containers
= $('.tabMenuContainer:not(.staticTabMenuContainer)');
3753 $containers
.each(function(index
, tabMenu
) {
3754 var $tabMenu
= $(tabMenu
);
3755 var $containerID
= $tabMenu
.wcfIdentify();
3756 if (self
._containers
[$containerID
]) {
3757 // continue with next container
3761 if ($tabMenu
.data('store') && !$('#' + $tabMenu
.data('store')).length
) {
3762 $('<input type="hidden" name="' + $tabMenu
.data('store') + '" value="" id="' + $tabMenu
.data('store') + '" />').appendTo($tabMenu
.parents('form').find('.formSubmit'));
3765 // init jQuery UI TabMenu
3766 self
._containers
[$containerID
] = $tabMenu
;
3769 activate: function(event
, eventData
) {
3770 var $panel
= $(eventData
.newPanel
);
3771 var $container
= $panel
.closest('.tabMenuContainer');
3773 // store currently selected item
3774 var $tabMenu
= $container
;
3776 // do not trigger on init
3777 if ($tabMenu
.data('isParent') === undefined) {
3781 if ($tabMenu
.data('isParent')) {
3782 if ($tabMenu
.data('store')) {
3783 $('#' + $tabMenu
.data('store')).val($panel
.attr('id'));
3789 $tabMenu
= $tabMenu
.data('parent');
3793 // set panel id as location hash
3794 if (WCF
.TabMenu
._didInit
) {
3795 // do not update history if within an overlay
3796 if ($panel
.data('inTabMenu') == undefined) {
3797 $panel
.data('inTabMenu', ($panel
.parents('.dialogContainer').length
));
3800 if (!$panel
.data('inTabMenu')) {
3801 if (window
.history
) {
3802 window
.history
.pushState(null, document
.title
, window
.location
.toString().replace(/#.+$/, '') + '#' + $panel
.attr('id'));
3805 location
.hash
= '#' + $panel
.attr('id');
3812 $tabMenu
.data('isParent', ($tabMenu
.children('.tabMenuContainer, .tabMenuContent').length
> 0)).data('parent', false);
3813 if (!$tabMenu
.data('isParent')) {
3814 // check if we're a child element
3815 if ($tabMenu
.parent().hasClass('tabMenuContainer')) {
3816 $tabMenu
.data('parent', $tabMenu
.parent());
3821 // try to resolve location hash
3822 if (!this._didInit
) {
3823 this._selectActiveTab();
3824 $(window
).bind('hashchange', $.proxy(this.selectTabs
, this));
3826 if (!this._selectErroneousTab()) {
3830 if ($.browser
.mozilla
&& location
.hash
) {
3831 var $target
= $(location
.hash
);
3832 if ($target
.length
&& $target
.hasClass('tabMenuContent')) {
3833 var $offset
= $target
.offset();
3834 window
.scrollTo($offset
.left
, $offset
.top
);
3839 this._didInit
= true;
3843 * Reloads the tab menus.
3845 reload: function() {
3846 this._containers
= { };
3851 * Force display of first erroneous tab and returns true if at least one
3852 * tab contains an error.
3856 _selectErroneousTab: function() {
3857 var $foundErrors
= false;
3858 for (var $containerID
in this._containers
) {
3859 var $tabMenu
= this._containers
[$containerID
];
3861 if ($tabMenu
.find('.formError').length
) {
3862 $foundErrors
= true;
3864 if (!$tabMenu
.data('isParent')) {
3866 if ($tabMenu
.data('parent') === false) {
3870 $tabMenu
= $tabMenu
.data('parent').wcfTabs('selectTab', $tabMenu
.wcfIdentify());
3878 // found an error in a non-nested tab menu
3880 for (var $containerID
in this._containers
) {
3881 var $tabMenu
= this._containers
[$containerID
];
3882 var $formError
= $tabMenu
.find('.formError:eq(0)');
3884 if ($formError
.length
) {
3885 // find the tab container
3886 $tabMenu
.wcfTabs('selectTab', $formError
.parents('.tabMenuContent').wcfIdentify());
3889 if ($tabMenu
.data('parent') === false) {
3893 $tabMenu
= $tabMenu
.data('parent').wcfTabs('selectTab', $tabMenu
.wcfIdentify());
3905 * Selects the active tab menu item.
3907 _selectActiveTab: function() {
3908 for (var $containerID
in this._containers
) {
3909 var $tabMenu
= this._containers
[$containerID
];
3910 if ($tabMenu
.data('active')) {
3911 var $index
= $tabMenu
.data('active');
3912 var $subIndex
= null;
3913 if (/-/.test($index
)) {
3914 var $tmp
= $index
.split('-');
3916 $subIndex
= $tmp
[1];
3919 $tabMenu
.find('.tabMenuContent').each(function(innerIndex
, tabMenuItem
) {
3920 var $tabMenuItem
= $(tabMenuItem
);
3921 if ($tabMenuItem
.wcfIdentify() == $index
) {
3922 $tabMenu
.wcfTabs('select', innerIndex
);
3923 if ($subIndex
!== null) {
3924 if ($tabMenuItem
.hasClass('tabMenuContainer')) {
3925 $tabMenuItem
.wcfTabs('selectTab', $tabMenu
.data('active'));
3928 $tabMenu
.wcfTabs('selectTab', $tabMenu
.data('active'));
3940 * Resolves location hash to display tab menus.
3944 selectTabs: function() {
3945 if (location
.hash
) {
3946 var $hash
= location
.hash
.substr(1);
3948 // try to find matching tab menu container
3949 var $tabMenu
= $('#' + $.wcfEscapeID($hash
));
3950 if ($tabMenu
.length
=== 1 && $tabMenu
.hasClass('ui-tabs-panel')) {
3951 $tabMenu
= $tabMenu
.parent('.ui-tabs');
3952 if ($tabMenu
.length
) {
3953 $tabMenu
.wcfTabs('selectTab', $hash
);
3955 // check if this is a nested tab menu
3956 if ($tabMenu
.hasClass('ui-tabs-panel')) {
3957 $hash
= $tabMenu
.wcfIdentify();
3958 $tabMenu
= $tabMenu
.parent('.ui-tabs');
3959 if ($tabMenu
.length
) {
3960 $tabMenu
.wcfTabs('selectTab', $hash
);
3974 * Templates that may be fetched more than once with different variables.
3975 * Based upon ideas from Prototype's template.
3978 * var myTemplate = new WCF.Template('{$hello} World');
3979 * myTemplate.fetch({ hello: 'Hi' }); // Hi World
3980 * myTemplate.fetch({ hello: 'Hello' }); // Hello World
3982 * my2ndTemplate = new WCF.Template('{@$html}{$html}');
3983 * my2ndTemplate.fetch({ html: '<b>Test</b>' }); // <b>Test</b><b>Test</b>
3985 * var my3rdTemplate = new WCF.Template('You can use {literal}{$variable}{/literal}-Tags here');
3986 * my3rdTemplate.fetch({ variable: 'Not shown' }); // You can use {$variable}-Tags here
3988 * @param template template-content
3989 * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/template.js
3991 WCF
.Template
= Class
.extend({
3995 * @param $template template-content
3997 init: function(template
) {
3998 var $literals
= new WCF
.Dictionary();
4001 // escape \ and ' and newlines
4002 template
= template
.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/(\r\n|\n|\r)/g, '\\n');
4004 // save literal-tags
4005 template = template.replace(/\{literal\}(.*?)\{\/literal\}/g, $.proxy(function(match) {
4006 // hopefully no one uses this string in one of his templates
4007 var id = '@@@@@@@@@@@'+Math.random()+'@@@@@@@@@@@';
4008 $literals.add(id, match.replace(/\{\/?literal\}/g, ''));
4014 template = template.replace(/\{\*.*?\*\}/g, '');
4016 var parseParameterList = function(parameterString) {
4017 var $chars = parameterString.split('');
4018 var $parameters = { };
4022 var $doubleQuoted = false;
4023 var $singleQuoted = false;
4024 var $escaped = false;
4026 for (var $i = 0, $max = $chars.length; $i < $max; $i++) {
4027 var $char = $chars[$i];
4028 if ($inName && $char != '=' && $char != ' ') $name += $char;
4029 else if ($inName && $char == '=') {
4031 $singleQuoted = false;
4032 $doubleQuoted = false;
4035 else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == ' ') {
4037 $parameters[$name] = $value;
4038 $value = $name = '';
4040 else if (!$inName && $singleQuoted && !$escaped && $char == "'") {
4041 $singleQuoted = false;
4044 else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == "'") {
4045 $singleQuoted = true;
4048 else if (!$inName && $doubleQuoted && !$escaped && $char == '"') {
4049 $doubleQuoted = false;
4052 else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == '"') {
4053 $doubleQuoted = true;
4056 else if (!$inName && ($doubleQuoted || $singleQuoted) && !$escaped && $char == '\\') {
4060 else if (!$inName) {
4065 $parameters[$name] = $value;
4067 if ($doubleQuoted || $singleQuoted || $escaped) throw new Error('Syntax error in parameterList: "' + parameterString + '"');
4072 var unescape = function(string) {
4073 return string.replace(/\\n/g, "\n").replace(/\\\\/g, '\\').replace(/\\'/g, "'");
4076 template = template.replace(/\{(\$[^\}]+?)\}/g, function(_, content) {
4077 content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])"));
4079 return "' + WCF
.String
.escapeHTML(" + content + ") + '";
4082 .replace(/\{#(\$[^\}]+?)\}/g, function(_, content) {
4083 content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])"));
4085 return "' + WCF
.String
.formatNumeric(" + content + ") + '";
4087 // Variable without escaping
4088 .replace(/\{@(\$[^\}]+?)\}/g, function(_, content) {
4089 content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])"));
4091 return "' + " + content + " + '";
4094 .replace(/{lang}(.+?){\/lang}/g, function(_, content) {
4095 return "' + WCF
.Language
.get('" + unescape(content) + "') + '";
4098 .replace(/\{if (.+?)\}/g, function(_, content) {
4099 content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])"));
4102 "if (" + content + ") {\n" +
4106 .replace(/\{else ?if (.+?)\}/g, function(_, content) {
4107 content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])"));
4111 "else if (" + content + ") {\n" +
4115 .replace(/\{implode (.+?)\}/g, function(_, content) {
4118 content = content.replace(/\\\\/g, '\\').replace(/\\'/g
, "'");
4119 var $parameters
= parseParameterList(content
);
4121 if (typeof $parameters
['from'] === 'undefined') throw new Error('Missing from attribute in implode-tag');
4122 if (typeof $parameters
['item'] === 'undefined') throw new Error('Missing item attribute in implode-tag');
4123 if (typeof $parameters
['glue'] === 'undefined') $parameters
['glue'] = "', '";
4125 $parameters
['from'] = $parameters
['from'].replace(/\$([^.\[\s]+)/g, "(v.$1)");
4128 "var $implode_" + $tagID
+ " = false;\n" +
4129 "for ($implodeKey_" + $tagID
+ " in " + $parameters
['from'] + ") {\n" +
4130 " v[" + $parameters
['item'] + "] = " + $parameters
['from'] + "[$implodeKey_" + $tagID
+ "];\n" +
4131 (typeof $parameters
['key'] !== 'undefined' ? " v[" + $parameters
['key'] + "] = $implodeKey_" + $tagID
+ ";\n" : "") +
4132 " if ($implode_" + $tagID
+ ") $output += " + $parameters
['glue'] + ";\n" +
4133 " $implode_" + $tagID
+ " = true;\n" +
4137 .replace(/\{foreach (.+?)\}/g, function(_
, content
) {
4140 content
= content
.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
4141 var $parameters = parseParameterList(content);
4143 if (typeof $parameters['from'] === 'undefined') throw new Error('Missing from attribute in foreach-tag');
4144 if (typeof $parameters['item'] === 'undefined') throw new Error('Missing item attribute in foreach-tag');
4145 $parameters['from'] = $parameters['from'].replace(/\$([^.\[\s]+)/g, "(v
.$1)");
4148 "$foreach_"+$tagID+" = false;\n" +
4149 "for ($foreachKey_" + $tagID + " in " + $parameters['from'] + ") {\n" +
4150 " $foreach_"+$tagID+" = true;\n" +
4153 "if ($foreach_"+$tagID+") {\n" +
4154 " for ($foreachKey_" + $tagID + " in " + $parameters['from'] + ") {\n" +
4155 " v[" + $parameters['item
'] + "] = " + $parameters['from'] + "[$foreachKey_" + $tagID + "];\n" +
4156 (typeof $parameters['key
'] !== 'undefined' ? " v[" + $parameters['key
'] + "] = $foreachKey_" + $tagID + ";\n" : "") +
4160 .replace(/\{foreachelse\}/g,
4169 .replace(/\{\/foreach\}/g,
4176 .replace(/\{else\}/g,
4182 // {/if} and {/implode}
4183 .replace(/\{\/(if|implode)\}/g,
4190 for (var key in WCF.Template.callbacks) {
4191 template = WCF.Template.callbacks[key](template);
4194 // insert delimiter tags
4195 template = template.replace('{ldelim}', '{').replace('{rdelim}', '}');
4197 $literals.each(function(pair) {
4198 template = template.replace(pair.key, pair.value);
4201 template = "$output
+= '" + template + "';";
4204 this.fetch = new Function("v
", "if (typeof v
!= 'object') { v
= {}; } v
.__window
= window
; v
.__wcf
= window
.WCF
; var $output
= ''; " + template + ' return $output;');
4207 console.debug("var $output
= ''; " + template + ' return $output;');
4213 * Fetches the template with the given variables.
4215 * @param v variables to insert
4216 * @return parsed template
4218 fetch: function(v) {
4219 // this will be replaced in the init function
4224 * Array of callbacks that will be called after parsing the included tags. Only applies to Templates compiled after the callback was added.
4226 * @var array<Function>
4228 WCF.Template.callbacks = [ ];
4233 * @param string element
4234 * @param array showItems
4235 * @param array hideItems
4236 * @param function callback
4238 WCF.ToggleOptions = Class.extend({
4247 * list of items to be shown
4254 * list of items to be hidden
4261 * callback after options were toggled
4268 * Initializes option toggle.
4270 * @param string element
4271 * @param array showItems
4272 * @param array hideItems
4273 * @param function callback
4275 init: function(element, showItems, hideItems, callback) {
4276 this._element = $('#' + element);
4277 this._showItems = showItems;
4278 this._hideItems = hideItems;
4279 if (callback !== undefined) {
4280 this._callback = callback;
4284 this._element.click($.proxy(this._toggle, this));
4286 // execute toggle on init
4293 _toggle: function() {
4294 if (!this._element.prop('checked')) return;
4296 for (var $i = 0, $length = this._showItems.length; $i < $length; $i++) {
4297 var $item = this._showItems[$i];
4299 $('#' + $item).show();
4302 for (var $i = 0, $length = this._hideItems.length; $i < $length; $i++) {
4303 var $item = this._hideItems[$i];
4305 $('#' + $item).hide();
4308 if (this._callback !== null) {
4315 * Namespace for all kind of collapsible containers.
4317 WCF.Collapsible = {};
4320 * Simple implementation for collapsible content, neither does it
4321 * store its state nor does it allow AJAX callbacks to fetch content.
4323 WCF.Collapsible.Simple = {
4325 * Initializes collapsibles.
4328 $('.jsCollapsible').each($.proxy(function(index, button) {
4329 this._initButton(button);
4334 * Binds an event listener on all buttons triggering the collapsible.
4336 * @param object button
4338 _initButton: function(button) {
4339 var $button = $(button);
4340 var $isOpen = $button.data('isOpen');
4343 // hide container on init
4344 $('#' + $button.data('collapsibleContainer')).hide();
4347 $button.click($.proxy(this._toggle, this));
4351 * Toggles collapsible containers on click.
4353 * @param object event
4355 _toggle: function(event) {
4356 var $button = $(event.currentTarget);
4357 var $isOpen = $button.data('isOpen');
4358 var $target = $('#' + $.wcfEscapeID($button.data('collapsibleContainer')));
4361 $target.stop().wcfBlindOut('vertical', $.proxy(function() {
4362 this._toggleImage($button);
4367 $target.stop().wcfBlindIn('vertical', $.proxy(function() {
4368 this._toggleImage($button);
4373 $button.data('isOpen', $isOpen);
4376 event.stopPropagation();
4381 * Toggles image of target button.
4383 * @param jQuery button
4385 _toggleImage: function(button) {
4386 var $icon = button.find('span.icon');
4387 if (button.data('isOpen')) {
4388 $icon.removeClass('icon-chevron-right').addClass('icon-chevron-down');
4391 $icon.removeClass('icon-chevron-down').addClass('icon-chevron-right');
4397 * Basic implementation for collapsible containers with AJAX support. Results for open
4398 * and closed state will be cached.
4400 * @param string className
4402 WCF.Collapsible.Remote = Class.extend({
4410 * list of active containers
4416 * container meta data
4423 * @var WCF.Action.Proxy
4428 * Initializes the controller for collapsible containers with AJAX support.
4430 * @param string className
4432 init: function(className) {
4433 this._className = className;
4434 this._proxy = new WCF.Action.Proxy({
4435 success: $.proxy(this._success, this)
4438 // initialize each container
4441 WCF.DOMNodeInsertedHandler.addCallback('WCF.Collapsible.Remote', $.proxy(this._init, this));
4445 * Initializes a collapsible container.
4447 * @param string containerID
4449 _init: function(containerID) {
4450 this._getContainers().each($.proxy(function(index, container) {
4451 var $container = $(container);
4452 var $containerID = $container.wcfIdentify();
4454 if (this._containers[$containerID] === undefined) {
4455 this._containers[$containerID] = $container;
4457 this._initContainer($containerID);
4463 * Initializes a collapsible container.
4465 * @param string containerID
4467 _initContainer: function(containerID) {
4468 var $target = this._getTarget(containerID);
4469 var $buttonContainer = this._getButtonContainer(containerID);
4470 var $button = this._createButton(containerID, $buttonContainer);
4472 // store container meta data
4473 this._containerData[containerID] = {
4475 buttonContainer: $buttonContainer,
4476 isOpen: this._containers[containerID].data('isOpen'),
4480 // add 'jsCollapsed' CSS class
4481 if (!this._containers[containerID].data('isOpen')) {
4482 $('#' + containerID).addClass('jsCollapsed');
4487 * Returns a collection of collapsible containers.
4491 _getContainers: function() { },
4494 * Returns the target element for current collapsible container.
4496 * @param integer containerID
4499 _getTarget: function(containerID) { },
4502 * Returns the button container for current collapsible container.
4504 * @param integer containerID
4507 _getButtonContainer: function(containerID) { },
4510 * Creates the toggle button.
4512 * @param integer containerID
4513 * @param jQuery buttonContainer
4515 _createButton: function(containerID, buttonContainer) {
4516 var $isOpen = this._containers[containerID].data('isOpen');
4517 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);
4518 $button.data('containerID', containerID).click($.proxy(this._toggleContainer, this));
4524 * Toggles a container.
4526 * @param object event
4528 _toggleContainer: function(event) {
4529 var $button = $(event.currentTarget);
4530 var $containerID = $button.data('containerID');
4531 var $isOpen = this._containerData[$containerID].isOpen;
4532 var $state = ($isOpen) ? 'open' : 'close';
4533 var $newState = ($isOpen) ? 'close' : 'open';
4535 // fetch content state via AJAX
4536 this._proxy.setOption('data', {
4537 actionName: 'loadContainer',
4538 className: this._className,
4539 interfaceName: 'wcf\\data\\ILoadableContainerAction',
4540 objectIDs: [ this._getObjectID($containerID) ],
4541 parameters: $.extend(true, {
4542 containerID: $containerID,
4543 currentState: $state,
4545 }, this._getAdditionalParameters($containerID))
4547 this._proxy.sendRequest();
4549 // toogle 'jsCollapsed' CSS class
4550 $('#' + $containerID).toggleClass('jsCollapsed');
4552 // set spinner for current button
4553 // this._exchangeIcon($button);
4557 * Exchanges button icon.
4559 * @param jQuery button
4560 * @param string newIcon
4562 _exchangeIcon: function(button, newIcon) {
4563 newIcon = newIcon || 'spinner';
4564 button.removeClass('icon-chevron-down icon-chevron-right icon-spinner').addClass('icon-' + newIcon);
4568 * Returns the object id for current container.
4570 * @param integer containerID
4573 _getObjectID: function(containerID) {
4574 return $('#' + containerID).data('objectID');
4578 * Returns additional parameters.
4580 * @param integer containerID
4583 _getAdditionalParameters: function(containerID) {
4588 * Updates container content.
4590 * @param integer containerID
4591 * @param string newContent
4592 * @param string newState
4594 _updateContent: function(containerID, newContent, newState) {
4595 this._containerData[containerID].target.html(newContent);
4599 * Sets content upon successfull AJAX request.
4601 * @param object data
4602 * @param string textStatus
4603 * @param jQuery jqXHR
4605 _success: function(data, textStatus, jqXHR) {
4606 // validate container id
4607 if (!data.returnValues.containerID) return;
4608 var $containerID = data.returnValues.containerID;
4610 // check if container id is known
4611 if (!this._containers[$containerID]) return;
4613 // update content storage
4614 this._containerData[$containerID].isOpen = (data.returnValues.isOpen) ? true : false;
4615 var $newState = (data.returnValues.isOpen) ? 'open' : 'close';
4617 // update container content
4618 this._updateContent($containerID, $.trim(data.returnValues.content), $newState);
4621 this._exchangeIcon(this._containerData[$containerID].button, (data.returnValues.isOpen ? 'chevron-down' : 'chevron-right'));
4626 * Basic implementation for collapsible containers with AJAX support. Requires collapsible
4627 * content to be available in DOM already, if you want to load content on the fly use
4628 * WCF.Collapsible.Remote instead.
4630 WCF.Collapsible.SimpleRemote = WCF.Collapsible.Remote.extend({
4632 * Initializes an AJAX-based collapsible handler.
4634 * @param string className
4636 init: function(className) {
4637 this._super(className);
4639 // override settings for action proxy
4640 this._proxy = new WCF.Action.Proxy({
4641 showLoadingOverlay: false
4646 * @see WCF.Collapsible.Remote._initContainer()
4648 _initContainer: function(containerID) {
4649 this._super(containerID);
4651 // hide container on init if applicable
4652 if (!this._containerData[containerID].isOpen) {
4653 this._containerData[containerID].target.hide();
4654 this._exchangeIcon(this._containerData[containerID].button, 'chevron-right');
4659 * Toggles container visibility.
4661 * @param object event
4663 _toggleContainer: function(event) {
4664 var $button = $(event.currentTarget);
4665 var $containerID = $button.data('containerID');
4666 var $isOpen = this._containerData[$containerID].isOpen;
4667 var $currentState = ($isOpen) ? 'open' : 'close';
4668 var $newState = ($isOpen) ? 'close' : 'open';
4670 this._proxy.setOption('data', {
4671 actionName: 'toggleContainer',
4672 className: this._className,
4673 interfaceName: 'wcf\\data\\IToggleContainerAction',
4674 objectIDs: [ this._getObjectID($containerID) ],
4675 parameters: $.extend(true, {
4676 containerID: $containerID,
4677 currentState: $currentState,
4679 }, this._getAdditionalParameters($containerID))
4681 this._proxy.sendRequest();
4684 this._exchangeIcon(this._containerData[$containerID].button, ($newState === 'open' ? 'chevron-down' : 'chevron-right'));
4687 if ($newState === 'open') {
4688 this._containerData[$containerID].target.show();
4691 this._containerData[$containerID].target.hide();
4694 // toogle 'jsCollapsed' CSS class
4695 $('#' + $containerID).toggleClass('jsCollapsed');
4697 // update container data
4698 this._containerData[$containerID].isOpen = ($newState === 'open' ? true : false);
4703 * Provides collapsible sidebars with persistency support.
4705 WCF.Collapsible.Sidebar = Class.extend({
4707 * trigger button object
4713 * trigger button height
4725 * main container object
4728 _mainContainer: null,
4732 * @var WCF.Action.Proxy
4749 * sidebar identifier
4755 * sidebar offset from document top
4764 _userPanelHeight: 0,
4767 * Creates a new WCF.Collapsible.Sidebar object.
4770 this._sidebar = $('.sidebar:eq(0)');
4771 if (!this._sidebar.length) {
4772 console.debug("[WCF
.Collapsible
.Sidebar
] Could not find sidebar
, aborting
.");
4776 this._isOpen = (this._sidebar.data('isOpen')) ? true : false;
4777 this._sidebarName = this._sidebar.data('sidebarName');
4778 this._mainContainer = $('#main');
4779 this._sidebarHeight = this._sidebar.height();
4780 this._sidebarOffset = this._sidebar.getOffsets('offset').top;
4781 this._userPanelHeight = $('#topMenu').outerHeight();
4783 // add toggle button
4784 this._button = $('<a class="collapsibleButton jsTooltip
" title="' + WCF.Language.get('wcf
.global
.button
.collapsible
') + '" />').prependTo(this._sidebar);
4785 this._button.wrap('<span />');
4786 this._button.click($.proxy(this._click, this));
4787 this._buttonHeight = this._button.outerHeight();
4789 WCF.DOMNodeInsertedHandler.execute();
4791 this._proxy = new WCF.Action.Proxy({
4792 showLoadingOverlay: false,
4793 url: 'index.php/AJAXInvoke/?t=' + SECURITY_TOKEN + SID_ARG_2ND
4796 $(document).scroll($.proxy(this._scroll, this)).resize($.proxy(this._scroll, this));
4798 this._renderSidebar();
4801 // fake resize event once transition has completed
4802 var $window = $(window);
4803 this._sidebar.on('webkitTransitionEnd transitionend msTransitionEnd oTransitionEnd', function() { $window.trigger('resize'); });
4807 * Handles clicks on the trigger button.
4809 _click: function() {
4810 this._isOpen = (this._isOpen) ? false : true;
4812 this._proxy.setOption('data', {
4813 actionName: 'toggle',
4814 className: 'wcf\\system\\user\\collapsible\\content\\UserCollapsibleSidebarHandler',
4815 isOpen: (this._isOpen ? 1 : 0),
4816 sidebarName: this._sidebarName
4818 this._proxy.sendRequest();
4820 this._renderSidebar();
4824 * Aligns the toggle button upon scroll or resize.
4826 _scroll: function() {
4827 var $window = $(window);
4828 var $scrollOffset = $window.scrollTop();
4830 // calculate top and bottom coordinates of visible sidebar
4831 var $topOffset = Math.max($scrollOffset - this._sidebarOffset, 0);
4832 var $bottomOffset = Math.min(this._mainContainer.height(), ($window.height() + $scrollOffset) - this._sidebarOffset);
4835 if ($bottomOffset === $topOffset) {
4836 // sidebar not within visible area
4837 $buttonTop = this._sidebarOffset + this._sidebarHeight;
4840 $buttonTop = $topOffset + (($bottomOffset - $topOffset) / 2);
4842 // if the user panel is above the sidebar, substract it's height
4843 var $overlap = Math.max(Math.min($topOffset - this._userPanelHeight, this._userPanelHeight), 0);
4845 $buttonTop += ($overlap / 2);
4849 // ensure the button does not exceed bottom boundaries
4850 if (($bottomOffset - $topOffset - this._userPanelHeight) < this._buttonHeight) {
4851 $buttonTop = $buttonTop - this._buttonHeight;
4854 // exclude half button height
4855 $buttonTop = Math.max($buttonTop - (this._buttonHeight / 2), 0);
4858 this._button.css({ top: $buttonTop + 'px' });
4863 * Renders the sidebar state.
4865 _renderSidebar: function() {
4867 $('.sidebarOrientationLeft, .sidebarOrientationRight').removeClass('sidebarCollapsed');
4870 $('.sidebarOrientationLeft, .sidebarOrientationRight').addClass('sidebarCollapsed');
4873 // update button position
4876 // IE9 does not support transitions, fire resize event manually
4877 if ($.browser.msie && $.browser.version.indexOf('9') === 0) {
4878 $(window).trigger('resize');
4884 * Holds userdata of the current user
4888 * id of the active user
4894 * name of the active user
4900 * Initializes userdata
4902 * @param integer userID
4903 * @param string username
4905 init: function(userID, username) {
4906 this.userID = userID;
4907 this.username = username;
4912 * Namespace for effect-related functions.
4917 * Scrolls to a specific element offset, optionally handling menu height.
4919 WCF.Effect.Scroll = Class.extend({
4921 * Scrolls to a specific element offset.
4923 * @param jQuery element
4924 * @param boolean excludeMenuHeight
4925 * @param boolean disableAnimation
4928 scrollTo: function(element, excludeMenuHeight, disableAnimation) {
4929 if (!element.length) {
4933 var $elementOffset = element.getOffsets('offset').top;
4934 var $documentHeight = $(document).height();
4935 var $windowHeight = $(window).height();
4937 // handles menu height
4938 /*if (excludeMenuHeight) {
4939 $elementOffset = Math.max($elementOffset - $('#topMenu').outerHeight(), 0);
4942 if ($elementOffset > $documentHeight - $windowHeight) {
4943 $elementOffset = $documentHeight - $windowHeight;
4944 if ($elementOffset < 0) {
4949 if (disableAnimation === true) {
4950 $('html,body').scrollTop($elementOffset);
4953 $('html,body').animate({ scrollTop: $elementOffset }, 400, function (x, t, b, c, d) {
4954 return -c * ( ( t = t / d - 1 ) * t * t * t - 1) + b;
4963 * Creates a smooth scroll effect.
4965 WCF.Effect.SmoothScroll = WCF.Effect.Scroll.extend({
4967 * Initializes effect.
4971 $(document).on('click', 'a[href$=#top],a[href$=#bottom]', function() {
4972 var $target = $(this.hash);
4973 self.scrollTo($target, true);
4981 * Creates the balloon tool-tip.
4983 WCF.Effect.BalloonTooltip = Class.extend({
4985 * initialization state
4997 * cache viewport dimensions
5000 _viewportDimensions: { },
5003 * Initializes tooltips.
5006 if (jQuery.browser.mobile) return;
5008 if (!this._didInit) {
5010 this._tooltip = $('<div id="balloonTooltip
" class="balloonTooltip
"><span id="balloonTooltipText
"></span><span class="pointer
"><span></span></span></div>').appendTo($('body')).hide();
5012 // get viewport dimensions
5013 this._updateViewportDimensions();
5015 // update viewport dimensions on resize
5016 $(window).resize($.proxy(this._updateViewportDimensions, this));
5018 // observe DOM changes
5019 WCF.DOMNodeInsertedHandler.addCallback('WCF.Effect.BalloonTooltip', $.proxy(this.init, this));
5021 this._didInit = true;
5025 $('.jsTooltip').each($.proxy(this._initTooltip, this));
5029 * Updates cached viewport dimensions.
5031 _updateViewportDimensions: function() {
5032 this._viewportDimensions = $(document).getDimensions();
5036 * Initializes a tooltip element.
5038 * @param integer index
5039 * @param object element
5041 _initTooltip: function(index, element) {
5042 var $element = $(element);
5044 if ($element.hasClass('jsTooltip')) {
5045 $element.removeClass('jsTooltip');
5046 var $title = $element.attr('title');
5048 // ignore empty elements
5049 if ($title !== '') {
5050 $element.data('tooltip', $title);
5051 $element.removeAttr('title');
5054 $.proxy(this._mouseEnterHandler, this),
5055 $.proxy(this._mouseLeaveHandler, this)
5057 $element.click($.proxy(this._mouseLeaveHandler, this));
5063 * Shows tooltip on hover.
5065 * @param object event
5067 _mouseEnterHandler: function(event) {
5068 var $element = $(event.currentTarget);
5070 var $title = $element.attr('title');
5071 if ($title && $title !== '') {
5072 $element.data('tooltip', $title);
5073 $element.removeAttr('title');
5076 // reset tooltip position
5082 // empty tooltip, skip
5083 if (!$element.data('tooltip')) {
5084 this._tooltip.hide();
5089 this._tooltip.children('span:eq(0)').text($element.data('tooltip'));
5092 var $arrow = this._tooltip.find('.pointer');
5095 this._tooltip.show();
5096 var $arrowWidth = $arrow.outerWidth();
5097 this._tooltip.hide();
5099 // calculate position
5100 var $elementOffsets = $element.getOffsets('offset');
5101 var $elementDimensions = $element.getDimensions('outer');
5102 var $tooltipDimensions = this._tooltip.getDimensions('outer');
5103 var $tooltipDimensionsInner = this._tooltip.getDimensions('inner');
5105 var $elementCenter = $elementOffsets.left + Math.ceil($elementDimensions.width / 2);
5106 var $tooltipHalfWidth = Math.ceil($tooltipDimensions.width / 2);
5108 // determine alignment
5109 var $alignment = 'center';
5110 if (($elementCenter - $tooltipHalfWidth) < 5) {
5111 $alignment = 'left';
5113 else if ((this._viewportDimensions.width - 5) < ($elementCenter + $tooltipHalfWidth)) {
5114 $alignment = 'right';
5117 // calculate top offset
5118 if ($elementOffsets.top + $elementDimensions.height + $tooltipDimensions.height - $(document).scrollTop() < $(window).height()) {
5119 var $top = $elementOffsets.top + $elementDimensions.height + 7;
5120 this._tooltip.removeClass('inverse');
5121 $arrow.css('top', -5);
5124 var $top = $elementOffsets.top - $tooltipDimensions.height - 7;
5125 this._tooltip.addClass('inverse');
5126 $arrow.css('top', $tooltipDimensions.height);
5129 // calculate left offset
5130 switch ($alignment) {
5132 var $left = Math.round($elementOffsets.left - $tooltipHalfWidth + ($elementDimensions.width / 2));
5135 left: ($tooltipDimensionsInner.width / 2 - $arrowWidth / 2) + "px
"
5140 var $left = $elementOffsets.left;
5148 var $left = $elementOffsets.left + $elementDimensions.width - $tooltipDimensions.width;
5151 left: ($tooltipDimensionsInner.width - $arrowWidth - 5) + "px
"
5163 this._tooltip.wcfFadeIn();
5167 * Hides tooltip once cursor left the element.
5169 * @param object event
5171 _mouseLeaveHandler: function(event) {
5172 this._tooltip.stop().hide().css({
5179 * Handles clicks outside an overlay, hitting body-tag through bubbling.
5181 * You should always remove callbacks before disposing the attached element,
5182 * preventing errors from blocking the iteration. Furthermore you should
5183 * always handle clicks on your overlay's container and return 'false' to
5186 WCF.CloseOverlayHandler = {
5189 * @var WCF.Dictionary
5191 _callbacks: new WCF.Dictionary(),
5194 * indicates that overlay handler is listening to click events on body-tag
5197 _isListening: false,
5200 * Adds a new callback.
5202 * @param string identifier
5203 * @param object callback
5205 addCallback: function(identifier, callback) {
5206 this._bindListener();
5208 if (this._callbacks.isset(identifier)) {
5209 console.debug("[WCF
.CloseOverlayHandler
] identifier
'" + identifier + "' is already bound to a callback
");
5213 this._callbacks.add(identifier, callback);
5217 * Removes a callback from list.
5219 * @param string identifier
5221 removeCallback: function(identifier) {
5222 if (this._callbacks.isset(identifier)) {
5223 this._callbacks.remove(identifier);
5228 * Binds click event handler.
5230 _bindListener: function() {
5231 if (this._isListening) return;
5233 $('body').click($.proxy(this._executeCallbacks, this));
5235 this._isListening = true;
5239 * Executes callbacks on click.
5241 _executeCallbacks: function(event) {
5242 this._callbacks.each(function(pair) {
5250 * Notifies objects once a DOM node was inserted.
5252 WCF.DOMNodeInsertedHandler = {
5255 * @var array<object>
5260 * prevent infinite loop if a callback manipulates DOM
5263 _isExecuting: false,
5266 * Adds a new callback.
5268 * @param string identifier
5269 * @param object callback
5271 addCallback: function(identifier, callback) {
5272 this._callbacks.push(callback);
5276 * Executes callbacks on click.
5278 _executeCallbacks: function() {
5279 if (this._isExecuting) return;
5281 // do not track events while executing callbacks
5282 this._isExecuting = true;
5284 for (var $i = 0, $length = this._callbacks.length; $i < $length; $i++) {
5285 this._callbacks[$i]();
5288 // enable listener again
5289 this._isExecuting = false;
5293 * Executes all callbacks.
5295 execute: function() {
5296 this._executeCallbacks();
5301 * Notifies objects once a DOM node was removed.
5303 WCF.DOMNodeRemovedHandler = {
5306 * @var WCF.Dictionary
5308 _callbacks: new WCF.Dictionary(),
5311 * prevent infinite loop if a callback manipulates DOM
5314 _isExecuting: false,
5317 * indicates that overlay handler is listening to DOMNodeRemoved events on body-tag
5320 _isListening: false,
5323 * Adds a new callback.
5325 * @param string identifier
5326 * @param object callback
5328 addCallback: function(identifier, callback) {
5329 this._bindListener();
5331 if (this._callbacks.isset(identifier)) {
5332 console.debug("[WCF
.DOMNodeRemovedHandler
] identifier
'" + identifier + "' is already bound to a callback
");
5336 this._callbacks.add(identifier, callback);
5340 * Removes a callback from list.
5342 * @param string identifier
5344 removeCallback: function(identifier) {
5345 if (this._callbacks.isset(identifier)) {
5346 this._callbacks.remove(identifier);
5351 * Binds click event handler.
5353 _bindListener: function() {
5354 if (this._isListening) return;
5356 $(document).bind('DOMNodeRemoved', $.proxy(this._executeCallbacks, this));
5358 this._isListening = true;
5362 * Executes callbacks if a DOM node is removed.
5364 _executeCallbacks: function(event) {
5365 if (this._isExecuting) return;
5367 // do not track events while executing callbacks
5368 this._isExecuting = true;
5370 this._callbacks.each(function(pair) {
5375 // enable listener again
5376 this._isExecuting = false;
5380 WCF.PageVisibilityHandler = {
5383 * @var WCF.Dictionary
5385 _callbacks: new WCF.Dictionary(),
5388 * indicates that event listeners are bound
5391 _isListening: false,
5394 * name of window's hidden property
5397 _hiddenFieldName: '',
5400 * Adds a new callback.
5402 * @param string identifier
5403 * @param object callback
5405 addCallback: function(identifier, callback) {
5406 this._bindListener();
5408 if (this._callbacks.isset(identifier)) {
5409 console.debug("[WCF
.PageVisibilityHandler
] identifier
'" + identifier + "' is already bound to a callback
");
5413 this._callbacks.add(identifier, callback);
5417 * Removes a callback from list.
5419 * @param string identifier
5421 removeCallback: function(identifier) {
5422 if (this._callbacks.isset(identifier)) {
5423 this._callbacks.remove(identifier);
5428 * Binds click event handler.
5430 _bindListener: function() {
5431 if (this._isListening) return;
5433 var $eventName = null;
5434 if (typeof document.hidden !== "undefined") {
5435 this._hiddenFieldName = "hidden
";
5436 $eventName = "visibilitychange
";
5438 else if (typeof document.mozHidden !== "undefined") {
5439 this._hiddenFieldName = "mozHidden
";
5440 $eventName = "mozvisibilitychange
";
5442 else if (typeof document.msHidden !== "undefined") {
5443 this._hiddenFieldName = "msHidden
";
5444 $eventName = "msvisibilitychange
";
5446 else if (typeof document.webkitHidden !== "undefined") {
5447 this._hiddenFieldName = "webkitHidden
";
5448 $eventName = "webkitvisibilitychange
";
5451 if ($eventName === null) {
5452 console.debug("[WCF
.PageVisibilityHandler
] This browser does not support the page visibility API
.");
5455 $(document).on($eventName, $.proxy(this._executeCallbacks, this));
5458 this._isListening = true;
5462 * Executes callbacks if page is hidden/visible again.
5464 _executeCallbacks: function(event) {
5465 if (this._isExecuting) return;
5467 // do not track events while executing callbacks
5468 this._isExecuting = true;
5470 var $state = document[this._hiddenFieldName];
5471 this._callbacks.each(function(pair) {
5476 // enable listener again
5477 this._isExecuting = false;
5482 * Namespace for table related classes.
5487 * Handles empty tables which can be used in combination with WCF.Action.Proxy.
5489 WCF.Table.EmptyTableHandler = Class.extend({
5497 * class name of the relevant rows
5503 * Initalizes a new WCF.Table.EmptyTableHandler object.
5505 * @param jQuery tableContainer
5506 * @param string rowClassName
5507 * @param object options
5509 init: function(tableContainer, rowClassName, options) {
5510 this._rowClassName = rowClassName;
5511 this._tableContainer = tableContainer;
5513 this._options = $.extend(true, {
5515 messageType: 'info',
5517 updatePageNumber: false
5520 WCF.DOMNodeRemovedHandler.addCallback('WCF.Table.EmptyTableHandler.' + rowClassName, $.proxy(this._remove, this));
5524 * Handles the removal of a DOM node.
5526 _remove: function(event) {
5527 var element = $(event.target);
5529 // check if DOM element is relevant
5530 if (element.hasClass(this._rowClassName)) {
5531 var tbody = element.parents('tbody:eq(0)');
5533 // check if table will be empty if DOM node is removed
5534 if (tbody.children('tr').length == 1) {
5535 if (this._options.emptyMessage) {
5537 this._tableContainer.replaceWith($('<p />').addClass(this._options.messageType).text(this._options.emptyMessage));
5539 else if (this._options.refreshPage) {
5541 if (this._options.updatePageNumber) {
5542 // calculate the new page number
5543 var pageNumberURLComponents = window.location.href.match(/(\?|&)pageNo=(\d+)/g);
5544 if (pageNumberURLComponents) {
5545 var currentPageNumber = pageNumberURLComponents[pageNumberURLComponents.length - 1].match(/\d+/g);
5546 if (this._options.updatePageNumber > 0) {
5547 currentPageNumber++;
5550 currentPageNumber--;
5553 window.location = window.location.href.replace(pageNumberURLComponents[pageNumberURLComponents.length - 1], pageNumberURLComponents[pageNumberURLComponents.length - 1][0] + 'pageNo=' + currentPageNumber);
5557 window.location.reload();
5561 // simply remove the table container
5562 this._tableContainer.remove();
5570 * Namespace for search related classes.
5575 * Performs a quick search.
5577 WCF.Search.Base = Class.extend({
5579 * notification callback
5591 * comma seperated list
5594 _commaSeperated: false,
5597 * delay in miliseconds before a request is send to the server
5603 * list with values that are excluded from seaching
5606 _excludedSearchValues: [],
5609 * count of available results
5615 * item index, -1 if none is selected
5627 * old search string, used for comparison
5628 * @var array<string>
5630 _oldSearchString: [ ],
5634 * @var WCF.Action.Proxy
5639 * search input field
5645 * minimum search input length, MUST be 1 or higher
5652 * @var WCF.PeriodicalExecuter
5657 * Initializes a new search.
5659 * @param jQuery searchInput
5660 * @param object callback
5661 * @param array excludedSearchValues
5662 * @param boolean commaSeperated
5663 * @param boolean showLoadingOverlay
5665 init: function(searchInput, callback, excludedSearchValues, commaSeperated, showLoadingOverlay) {
5666 if (callback !== null && callback !== undefined && !$.isFunction(callback)) {
5667 console.debug("[WCF
.Search
.Base
] The given callback is invalid
, aborting
.");
5671 this._callback = (callback) ? callback : null;
5673 this._excludedSearchValues = [];
5674 if (excludedSearchValues) {
5675 this._excludedSearchValues = excludedSearchValues;
5678 this._searchInput = $(searchInput);
5679 if (!this._searchInput.length) {
5680 console.debug("[WCF
.Search
.Base
] Selector
'" + searchInput + "' for search input is invalid
, aborting
.");
5684 this._searchInput.keydown($.proxy(this._keyDown, this)).keyup($.proxy(this._keyUp, this)).wrap('<span class="dropdown
" />');
5686 if ($.browser.mozilla && $.browser.touch) {
5687 this._searchInput.on('input', $.proxy(this._keyUp, this));
5690 this._list = $('<ul class="dropdownMenu
" />').insertAfter(this._searchInput);
5691 this._commaSeperated = (commaSeperated) ? true : false;
5692 this._oldSearchString = [ ];
5694 this._itemCount = 0;
5695 this._itemIndex = -1;
5697 this._proxy = new WCF.Action.Proxy({
5698 showLoadingOverlay: (showLoadingOverlay !== true ? false : true),
5699 success: $.proxy(this._success, this),
5700 autoAbortPrevious: true
5703 if (this._searchInput.is('input')) {
5704 this._searchInput.attr('autocomplete', 'off');
5707 this._searchInput.blur($.proxy(this._blur, this));
5709 WCF.Dropdown.initDropdownFragment(this._searchInput.parent(), this._list);
5713 * Closes the dropdown after a short delay.
5717 new WCF.PeriodicalExecuter(function(pe) {
5718 if (self._list.is(':visible')) {
5719 self._clearList(false);
5727 * Blocks execution of 'Enter' event.
5729 * @param object event
5731 _keyDown: function(event) {
5732 if (event.which === $.ui.keyCode.ENTER) {
5733 var $dropdown = this._searchInput.parents('.dropdown');
5735 if ($dropdown.data('disableAutoFocus')) {
5736 if (this._itemIndex !== -1) {
5737 event.preventDefault();
5740 else if ($dropdown.data('preventSubmit') || this._itemIndex !== -1) {
5741 event.preventDefault();
5747 * Performs a search upon key up.
5749 * @param object event
5751 _keyUp: function(event) {
5752 // handle arrow keys and return key
5753 switch (event.which) {
5754 case 37: // arrow-left
5755 case 39: // arrow-right
5759 case 38: // arrow up
5760 this._selectPreviousItem();
5764 case 40: // arrow down
5765 this._selectNextItem();
5769 case 13: // return key
5770 return this._selectElement(event);
5774 var $content = this._getSearchString(event);
5775 if ($content === '') {
5776 this._clearList(true);
5778 else if ($content.length >= this._triggerLength) {
5781 excludedSearchValues: this._excludedSearchValues,
5782 searchString: $content
5787 if (this._timer !== null) {
5792 this._timer = new WCF.PeriodicalExecuter(function() {
5793 self._queryServer($parameters);
5800 this._queryServer($parameters);
5804 // input below trigger length
5805 this._clearList(false);
5810 * Queries the server.
5812 * @param object parameters
5814 _queryServer: function(parameters) {
5815 this._searchInput.parents('.searchBar').addClass('loading');
5816 this._proxy.setOption('data', {
5817 actionName: 'getSearchResultList',
5818 className: this._className,
5819 interfaceName: 'wcf\\data\\ISearchAction',
5820 parameters: this._getParameters(parameters)
5822 this._proxy.sendRequest();
5826 * Sets query delay in miliseconds.
5828 * @param integer delay
5830 setDelay: function(delay) {
5831 this._delay = delay;
5835 * Selects the next item in list.
5837 _selectNextItem: function() {
5838 if (this._itemCount === 0) {
5842 // remove previous marking
5844 if (this._itemIndex === this._itemCount) {
5845 this._itemIndex = 0;
5848 this._highlightSelectedElement();
5852 * Selects the previous item in list.
5854 _selectPreviousItem: function() {
5855 if (this._itemCount === 0) {
5860 if (this._itemIndex === -1) {
5861 this._itemIndex = this._itemCount - 1;
5864 this._highlightSelectedElement();
5868 * Highlights the active item.
5870 _highlightSelectedElement: function() {
5871 this._list.find('li').removeClass('dropdownNavigationItem');
5872 this._list.find('li:eq(' + this._itemIndex + ')').addClass('dropdownNavigationItem');
5876 * Selects the active item by pressing the return key.
5878 * @param object event
5881 _selectElement: function(event) {
5882 if (this._itemCount === 0) {
5886 this._list.find('li.dropdownNavigationItem').trigger('click');
5892 * Returns search string.
5896 _getSearchString: function(event) {
5897 var $searchString = $.trim(this._searchInput.val());
5898 if (this._commaSeperated) {
5899 var $keyCode = event.keyCode || event.which;
5900 if ($keyCode == $.ui.keyCode.COMMA) {
5901 // ignore event if char is ','
5905 var $current = $searchString.split(',');
5906 var $length = $current.length;
5907 for (var $i = 0; $i < $length; $i++) {
5908 // remove whitespaces at the beginning or end
5909 $current[$i] = $.trim($current[$i]);
5912 for (var $i = 0; $i < $length; $i++) {
5913 var $part = $current[$i];
5915 if (this._oldSearchString[$i]) {
5917 if ($part != this._oldSearchString[$i]) {
5918 // current part was changed
5919 $searchString = $part;
5924 // new part was added
5925 $searchString = $part;
5930 this._oldSearchString = $current;
5933 return $searchString;
5937 * Returns parameters for quick search.
5939 * @param object parameters
5942 _getParameters: function(parameters) {
5947 * Evalutes search results.
5949 * @param object data
5950 * @param string textStatus
5951 * @param jQuery jqXHR
5953 _success: function(data, textStatus, jqXHR) {
5954 this._clearList(false);
5955 this._searchInput.parents('.searchBar').removeClass('loading');
5957 if ($.getLength(data.returnValues)) {
5958 for (var $i in data.returnValues) {
5959 var $item = data.returnValues[$i];
5961 this._createListItem($item);
5964 else if (!this._handleEmptyResult()) {
5968 WCF.CloseOverlayHandler.addCallback('WCF.Search.Base', $.proxy(function() { this._clearList(); }, this));
5970 var $containerID = this._searchInput.parents('.dropdown').wcfIdentify();
5971 if (!WCF.Dropdown.getDropdownMenu($containerID).hasClass('dropdownOpen')) {
5972 WCF.Dropdown.toggleDropdown($containerID);
5975 // pre-select first item
5976 this._itemIndex = -1;
5977 if (!WCF.Dropdown.getDropdown($containerID).data('disableAutoFocus')) {
5978 this._selectNextItem();
5983 * Handles empty result lists, should return false if dropdown should be hidden.
5987 _handleEmptyResult: function() {
5992 * Creates a new list item.
5994 * @param object item
5997 _createListItem: function(item) {
5998 var $listItem = $('<li><span>' + WCF.String.escapeHTML(item.label) + '</span></li>').appendTo(this._list);
5999 $listItem.data('objectID', item.objectID).data('label', item.label).click($.proxy(this._executeCallback, this));
6007 * Executes callback upon result click.
6009 * @param object event
6011 _executeCallback: function(event) {
6012 var $clearSearchInput = false;
6013 var $listItem = $(event.currentTarget);
6015 if (this._commaSeperated) {
6016 // auto-complete current part
6017 var $result = $listItem.data('label');
6018 for (var $i = 0, $length = this._oldSearchString.length; $i < $length; $i++) {
6019 var $part = this._oldSearchString[$i];
6020 if ($result.toLowerCase().indexOf($part.toLowerCase()) === 0) {
6021 this._oldSearchString[$i] = $result;
6022 this._searchInput.val(this._oldSearchString.join(', '));
6024 if ($.browser.webkit) {
6025 // chrome won't display the new value until the textarea is rendered again
6026 // this quick fix forces chrome to render it again, even though it changes nothing
6027 this._searchInput.css({ display: 'block' });
6030 // set focus on input field again
6031 var $position = this._searchInput.val().toLowerCase().indexOf($result.toLowerCase()) + $result.length;
6032 this._searchInput.focus().setCaret($position);
6039 if (this._callback === null) {
6040 this._searchInput.val($listItem.data('label'));
6043 $clearSearchInput = (this._callback($listItem.data()) === true) ? true : false;
6047 // close list and revert input
6048 this._clearList($clearSearchInput);
6052 * Closes the suggestion list and clears search input on demand.
6054 * @param boolean clearSearchInput
6056 _clearList: function(clearSearchInput) {
6057 if (clearSearchInput && !this._commaSeperated) {
6058 this._searchInput.val('');
6062 WCF.Dropdown.getDropdown(this._searchInput.parents('.dropdown').wcfIdentify()).removeClass('dropdownOpen');
6063 WCF.Dropdown.getDropdownMenu(this._searchInput.parents('.dropdown').wcfIdentify()).removeClass('dropdownOpen');
6065 this._list.end().empty();
6067 WCF.CloseOverlayHandler.removeCallback('WCF.Search.Base');
6069 // reset item navigation
6070 this._itemCount = 0;
6071 this._itemIndex = -1;
6075 * Adds an excluded search value.
6077 * @param string value
6079 addExcludedSearchValue: function(value) {
6080 if (!WCF.inArray(value, this._excludedSearchValues)) {
6081 this._excludedSearchValues.push(value);
6086 * Removes an excluded search value.
6088 * @param string value
6090 removeExcludedSearchValue: function(value) {
6091 var index = $.inArray(value, this._excludedSearchValues);
6093 this._excludedSearchValues.splice(index, 1);
6099 * Provides quick search for users and user groups.
6101 * @see WCF.Search.Base
6103 WCF.Search.User = WCF.Search.Base.extend({
6105 * @see WCF.Search.Base._className
6107 _className: 'wcf\\data\\user\\UserAction',
6110 * include user groups in search
6113 _includeUserGroups: false,
6116 * @see WCF.Search.Base.init()
6118 init: function(searchInput, callback, includeUserGroups, excludedSearchValues, commaSeperated) {
6119 this._includeUserGroups = includeUserGroups;
6121 this._super(searchInput, callback, excludedSearchValues, commaSeperated);
6125 * @see WCF.Search.Base._getParameters()
6127 _getParameters: function(parameters) {
6128 parameters.data.includeUserGroups = this._includeUserGroups ? 1 : 0;
6134 * @see WCF.Search.Base._createListItem()
6136 _createListItem: function(item) {
6137 var $listItem = this._super(item);
6141 $icon = $(item.icon);
6143 else if (this._includeUserGroups && item.type === 'group') {
6144 $icon = $('<span class="icon icon16 icon
-group
" />');
6148 var $label = $listItem.find('span').detach();
6150 var $box16 = $('<div />').addClass('box16').appendTo($listItem);
6152 $box16.append($icon);
6153 $box16.append($('<div />').append($label));
6157 $listItem.data('type', item.type);
6164 * Namespace for system-related classes.
6169 * Namespace for dependency-related classes.
6171 WCF.System.Dependency = { };
6174 * JavaScript Dependency Manager.
6176 WCF.System.Dependency.Manager = {
6178 * list of callbacks grouped by identifier
6184 * list of loaded identifiers
6185 * @var array<string>
6190 * list of setup callbacks grouped by identifier
6193 _setupCallbacks: { },
6196 * Registers a callback for given identifier, will be executed after all setup
6197 * callbacks have been invoked.
6199 * @param string identifier
6200 * @param object callback
6202 register: function(identifier, callback) {
6203 if (!$.isFunction(callback)) {
6204 console.debug("[WCF
.System
.Dependency
.Manager
] Callback
for identifier
'" + identifier + "' is invalid
, aborting
.");
6208 // already loaded, invoke now
6209 if (WCF.inArray(identifier, this._loaded)) {
6210 setTimeout(function() {
6215 if (!this._callbacks[identifier]) {
6216 this._callbacks[identifier] = [ ];
6219 this._callbacks[identifier].push(callback);
6224 * Registers a setup callback for given identifier, will be invoked
6225 * prior to all other callbacks.
6227 * @param string identifier
6228 * @param object callback
6230 setup: function(identifier, callback) {
6231 if (!$.isFunction(callback)) {
6232 console.debug("[WCF
.System
.Dependency
.Manager
] Setup callback
for identifier
'" + identifier + "' is invalid
, aborting
.");
6236 if (!this._setupCallbacks[identifier]) {
6237 this._setupCallbacks[identifier] = [ ];
6240 this._setupCallbacks[identifier].push(callback);
6244 * Invokes all callbacks for given identifier and marks it as loaded.
6246 * @param string identifier
6248 invoke: function(identifier) {
6249 if (this._setupCallbacks[identifier]) {
6250 for (var $i = 0, $length = this._setupCallbacks[identifier].length; $i < $length; $i++) {
6251 this._setupCallbacks[identifier][$i]();
6254 delete this._setupCallbacks[identifier];
6257 this._loaded.push(identifier);
6259 if (this._callbacks[identifier]) {
6260 for (var $i = 0, $length = this._callbacks[identifier].length; $i < $length; $i++) {
6261 this._callbacks[identifier][$i]();
6264 delete this._callbacks[identifier];
6270 * Provides flexible dropdowns for tab-based menus.
6272 WCF.System.FlexibleMenu = {
6274 * list of containers
6275 * @var object<jQuery>
6280 * list of registered container ids
6281 * @var array<string>
6287 * @var object<jQuery>
6292 * list of dropdown menus
6293 * @var object<jQuery>
6295 _dropdownMenus: { },
6298 * list of hidden status for containers
6299 * @var object<boolean>
6301 _hasHiddenItems: { },
6304 * true if menus are currently rebuilt
6310 * list of tab menu items per container
6311 * @var object<jQuery>
6316 * Initializes the WCF.System.FlexibleMenu class.
6319 // register .mainMenu and .navigationHeader by default
6320 this.registerMenu('mainMenu');
6321 this.registerMenu($('.navigationHeader:eq(0)').wcfIdentify());
6323 this._registerTabMenus();
6325 $(window).resize($.proxy(this.rebuildAll, this));
6326 WCF.DOMNodeInsertedHandler.addCallback('WCF.System.FlexibleMenu', $.proxy(this._registerTabMenus, this));
6330 * Registers tab menus.
6332 _registerTabMenus: function() {
6333 // register tab menus
6334 $('.tabMenuContainer:not(.jsFlexibleMenuEnabled)').each(function(index, tabMenuContainer) {
6335 var $navigation = $(tabMenuContainer).children('nav');
6336 if ($navigation.length && $navigation.find('> ul:eq(0) > li').length) {
6337 WCF.System.FlexibleMenu.registerMenu($navigation.wcfIdentify());
6343 * Registers a tab-based menu by id.
6347 * <ul style="white
-space
: nowrap
">
6355 * @param string containerID
6357 registerMenu: function(containerID) {
6358 var $container = $('#' + containerID);
6359 if (!$container.length) {
6360 console.debug("[WCF
.System
.FlexibleMenu
] Unable to find container identified by
'" + containerID + "', aborting
.");
6364 this._containerIDs.push(containerID);
6365 this._containers[containerID] = $container;
6366 this._menuItems[containerID] = $container.find('> ul:eq(0) > li');
6367 this._dropdowns[containerID] = $('<li class="dropdown
"><a class="icon icon16 icon
-list
" /></li>').data('containerID', containerID).click($.proxy(this._click, this));
6368 this._dropdownMenus[containerID] = $('<ul class="dropdownMenu
" />').appendTo(this._dropdowns[containerID]);
6369 this._hasHiddenItems[containerID] = false;
6371 this.rebuild(containerID);
6373 WCF.Dropdown.initDropdown(this._dropdowns[containerID].children('a'));
6377 * Rebuilds all registered containers.
6379 rebuildAll: function() {
6380 if (this._isWorking) {
6384 this._isWorking = true;
6386 for (var $i = 0, $length = this._containerIDs.length; $i < $length; $i++) {
6387 this.rebuild(this._containerIDs[$i]);
6390 this._isWorking = false;
6394 * Rebuilds a container, will be automatically invoked on window resize and registering.
6396 * @param string containerID
6398 rebuild: function(containerID) {
6399 if (!this._containers[containerID]) {
6400 console.debug("[WCF
.System
.FlexibleMenu
] Cannot rebuild unknown container identified by
'" + containerID + "'");
6404 var $changedItems = false;
6405 var $container = this._containers[containerID];
6406 var $currentWidth = 0;
6408 // the current width is based upon all items without the dropdown
6409 var $menuItems = this._menuItems[containerID].filter(':visible');
6410 for (var $i = 0, $length = $menuItems.length; $i < $length; $i++) {
6411 $currentWidth += $($menuItems[$i]).outerWidth(true);
6414 // insert dropdown for calculation purposes
6415 if (!this._hasHiddenItems[containerID]) {
6416 this._dropdowns[containerID].appendTo($container.children('ul:eq(0)'));
6419 var $dropdownWidth = this._dropdowns[containerID].outerWidth(true);
6421 // remove dropdown previously inserted
6422 if (!this._hasHiddenItems[containerID]) {
6423 this._dropdowns[containerID].detach();
6426 var $maximumWidth = $container.parent().innerWidth();
6428 // substract padding from the parent element
6429 $maximumWidth -= parseInt($container.parent().css('padding-left').replace(/px$/, '')) + parseInt($container.parent().css('padding-right').replace(/px$/, ''));
6431 // substract margins and paddings from the container itself
6432 $maximumWidth -= parseInt($container.css('margin-left').replace(/px$/, '')) + parseInt($container.css('margin-right').replace(/px$/, ''));
6433 $maximumWidth -= parseInt($container.css('padding-left').replace(/px$/, '')) + parseInt($container.css('padding-right').replace(/px$/, ''));
6435 // substract paddings from the actual list
6436 $maximumWidth -= parseInt($container.children('ul:eq(0)').css('padding-left').replace(/px$/, '')) + parseInt($container.children('ul:eq(0)').css('padding-right').replace(/px$/, ''));
6437 if ($currentWidth > $maximumWidth || (this._hasHiddenItems[containerID] && ($currentWidth > $maximumWidth - $dropdownWidth))) {
6438 var $menuItems = $menuItems.filter(':not(.active):not(.ui-state-active):visible');
6440 // substract dropdown width from maximum width
6441 $maximumWidth -= $dropdownWidth;
6443 // hide items starting with the last in list (ignores active item)
6444 for (var $i = ($menuItems.length - 1); $i >= 0; $i--) {
6445 if ($currentWidth > $maximumWidth) {
6446 var $item = $($menuItems[$i]);
6447 $currentWidth -= $item.outerWidth(true);
6450 $changedItems = true;
6451 this._hasHiddenItems[containerID] = true;
6458 if (this._hasHiddenItems[containerID]) {
6459 this._dropdowns[containerID].appendTo($container.children('ul:eq(0)'));
6462 else if (this._hasHiddenItems[containerID] && $currentWidth < $maximumWidth) {
6463 var $hiddenItems = this._menuItems[containerID].filter(':not(:visible)');
6465 // substract dropdown width from maximum width unless it is the last item
6466 $maximumWidth -= $dropdownWidth;
6468 // reverts items starting with the first hidden one
6469 for (var $i = 0, $length = $hiddenItems.length; $i < $length; $i++) {
6470 var $item = $($hiddenItems[$i]);
6471 $currentWidth += $item.outerWidth();
6473 if ($i + 1 == $length) {
6474 $maximumWidth += $dropdownWidth;
6477 if ($currentWidth < $maximumWidth) {
6478 // enough space, show item
6479 $item.css('display', '');
6480 $changedItems = true;
6487 if ($changedItems) {
6488 this._hasHiddenItems[containerID] = (this._menuItems[containerID].filter(':not(:visible)').length > 0);
6489 if (!this._hasHiddenItems[containerID]) {
6490 this._dropdowns[containerID].detach();
6495 // build dropdown menu for hidden items
6496 if ($changedItems) {
6497 this._dropdownMenus[containerID].empty();
6498 this._menuItems[containerID].filter(':not(:visible)').each($.proxy(function(index, item) {
6499 $('<li>' + $(item).html() + '</li>').appendTo(this._dropdownMenus[containerID]);
6506 * Namespace for mobile device-related classes.
6508 WCF.System.Mobile = { };
6511 * Handles general navigation and UX on mobile devices.
6513 WCF.System.Mobile.UX = {
6515 * true if mobile optimizations are enabled
6533 * Initializes the WCF.System.Mobile.UX class.
6536 this._enabled = false;
6537 this._main = $('#main');
6538 this._sidebar = this._main.find('> div > div > .sidebar');
6540 if ($.browser.touch) {
6541 $('html').addClass('touch');
6544 enquire.register('screen and (max-width: 800px)', {
6545 match: $.proxy(this._enable, this),
6546 unmatch: $.proxy(this._disable, this),
6547 setup: $.proxy(this._setup, this),
6551 if ($.browser.msie && this._sidebar.width() > 305) {
6552 // sidebar is rarely broken on IE9/IE10
6553 this._sidebar.css('display', 'none').css('display', '');
6558 * Initializes the mobile optimization once the media query matches.
6560 _setup: function() {
6561 this._initSidebarToggleButtons();
6562 this._initSearchBar();
6563 this._initButtonGroupNavigation();
6565 WCF.CloseOverlayHandler.addCallback('WCF.System.Mobile.UX', $.proxy(this._closeMenus, this));
6566 WCF.DOMNodeInsertedHandler.addCallback('WCF.System.Mobile.UX', $.proxy(this._initButtonGroupNavigation, this));
6570 * Enables the mobile optimization.
6572 _enable: function() {
6573 this._enabled = true;
6575 if ($.browser.msie) {
6576 this._sidebar.css('display', 'none').css('display', '');
6581 * Disables the mobile optimization.
6583 _disable: function() {
6584 this._enabled = false;
6586 if ($.browser.msie) {
6587 this._sidebar.css('display', 'none').css('display', '');
6592 * Initializes the sidebar toggle buttons.
6594 _initSidebarToggleButtons: function() {
6595 var $sidebarLeft = this._main.hasClass('sidebarOrientationLeft');
6596 var $sidebarRight = this._main.hasClass('sidebarOrientationRight');
6597 if ($sidebarLeft || $sidebarRight) {
6598 // use icons if language item is empty/non-existant
6599 var $languageShowSidebar = 'wcf.global.sidebar.show' + ($sidebarLeft ? 'Left' : 'Right') + 'Sidebar';
6600 if ($languageShowSidebar === WCF.Language.get($languageShowSidebar) || WCF.Language.get($languageShowSidebar) === '') {
6601 $languageShowSidebar = '<span class="icon icon16 icon
-double-angle
-' + ($sidebarLeft ? 'left
' : 'right
') + '" />';
6604 var $languageHideSidebar = 'wcf.global.sidebar.hide' + ($sidebarLeft ? 'Left' : 'Right') + 'Sidebar';
6605 if ($languageHideSidebar === WCF.Language.get($languageHideSidebar) || WCF.Language.get($languageHideSidebar) === '') {
6606 $languageHideSidebar = '<span class="icon icon16 icon
-double-angle
-' + ($sidebarLeft ? 'right
' : 'left
') + '" />';
6609 // add toggle buttons
6611 $('<span class="button small mobileSidebarToggleButton
">' + $languageShowSidebar + '</span>').appendTo($('.content')).click(function() { self._main.addClass('mobileShowSidebar'); });
6612 $('<span class="button small mobileSidebarToggleButton
">' + $languageHideSidebar + '</span>').appendTo($('.sidebar')).click(function() { self._main.removeClass('mobileShowSidebar'); });
6617 * Initializes the search bar.
6619 _initSearchBar: function() {
6620 var $searchBar = $('.searchBar:eq(0)');
6623 $searchBar.click(function() {
6624 if (self._enabled) {
6625 $searchBar.addClass('searchBarOpen');
6629 this._main.click(function() { $searchBar.removeClass('searchBarOpen'); });
6633 * Initializes the button group lists, converting them into native dropdowns.
6635 _initButtonGroupNavigation: function() {
6636 $('.buttonGroupNavigation:not(.jsMobileButtonGroupNavigation)').each(function(index, navigation) {
6637 var $navigation = $(navigation).addClass('jsMobileButtonGroupNavigation');
6638 var $button = $('<a class="dropdownLabel
"><span class="icon icon24 icon
-list
" /></a>').prependTo($navigation);
6640 $button.click(function() { $button.next().toggleClass('open'); return false; });
6647 _closeMenus: function() {
6648 $('.jsMobileButtonGroupNavigation > ul.open').removeClass('open');
6653 * System notification overlays.
6655 * @param string message
6656 * @param string cssClassNames
6658 WCF.System.Notification = Class.extend({
6660 * callback on notification close
6672 * notification message
6678 * notification overlay
6684 * Creates a new system notification overlay.
6686 * @param string message
6687 * @param string cssClassNames
6689 init: function(message, cssClassNames) {
6690 this._cssClassNames = cssClassNames || 'success';
6691 this._message = message || WCF.Language.get('wcf.global.success');
6692 this._overlay = $('#systemNotification');
6694 if (!this._overlay.length) {
6695 this._overlay = $('<div id="systemNotification
"><p></p></div>').hide().appendTo(document.body);
6700 * Shows the notification overlay.
6702 * @param object callback
6703 * @param integer duration
6704 * @param string message
6705 * @param string cssClassName
6707 show: function(callback, duration, message, cssClassNames) {
6708 duration = parseInt(duration);
6709 if (!duration) duration = 2000;
6711 if (callback && $.isFunction(callback)) {
6712 this._callback = callback;
6715 this._overlay.children('p').html((message || this._message));
6716 this._overlay.children('p').removeClass().addClass((cssClassNames || this._cssClassNames));
6718 // hide overlay after specified duration
6719 new WCF.PeriodicalExecuter($.proxy(this._hide, this), duration);
6721 this._overlay.wcfFadeIn(undefined, 300);
6725 * Hides the notification overlay after executing the callback.
6727 * @param WCF.PeriodicalExecuter pe
6729 _hide: function(pe) {
6730 if (this._callback !== null) {
6734 this._overlay.wcfFadeOut(undefined, 300);
6741 * Provides dialog-based confirmations.
6743 WCF.System.Confirmation = {
6745 * notification callback
6751 * confirmation dialog
6757 * callback parameters
6769 * confirmation button
6772 _confirmationButton: null,
6775 * Displays a confirmation dialog.
6777 * @param string message
6778 * @param object callback
6779 * @param object parameters
6780 * @param jQuery template
6782 show: function(message, callback, parameters, template) {
6783 if (this._visible) {
6784 console.debug('[WCF.System.Confirmation] Confirmation dialog is already open, refusing action.');
6788 if (!$.isFunction(callback)) {
6789 console.debug('[WCF.System.Confirmation] Given callback is invalid, aborting.');
6793 this._callback = callback;
6794 this._parameters = parameters;
6797 if (this._dialog === null) {
6798 this._createDialog();
6802 this._dialog.find('#wcfSystemConfirmationContent').empty().hide();
6803 if (template && template.length) {
6804 template.appendTo(this._dialog.find('#wcfSystemConfirmationContent').show());
6807 this._dialog.find('p').text(message);
6808 this._dialog.wcfDialog({
6809 onClose: $.proxy(this._close, this),
6810 onShow: $.proxy(this._show, this),
6811 title: WCF.Language.get('wcf.global.confirmation.title')
6814 this._dialog.wcfDialog('render');
6817 this._confirmationButton.focus();
6818 this._visible = true;
6822 * Creates the confirmation dialog on first use.
6824 _createDialog: function() {
6825 this._dialog = $('<div id="wcfSystemConfirmation
" class="systemConfirmation
"><p /><div id="wcfSystemConfirmationContent
" /></div>').hide().appendTo(document.body);
6826 var $formButtons = $('<div class="formSubmit
" />').appendTo(this._dialog);
6828 this._confirmationButton = $('<button class="buttonPrimary
">' + WCF.Language.get('wcf.global.confirmation.confirm') + '</button>').data('action', 'confirm').click($.proxy(this._click, this)).appendTo($formButtons);
6829 $('<button>' + WCF.Language.get('wcf.global.confirmation.cancel') + '</button>').data('action', 'cancel').click($.proxy(this._click, this)).appendTo($formButtons);
6833 * Handles button clicks.
6835 * @param object event
6837 _click: function(event) {
6838 this._notify($(event.currentTarget).data('action'));
6842 * Handles dialog being closed.
6844 _close: function() {
6845 if (this._visible) {
6846 this._notify('cancel');
6851 * Notifies callback upon user's decision.
6853 * @param string action
6855 _notify: function(action) {
6856 this._visible = false;
6857 this._dialog.wcfDialog('close');
6859 this._callback(action, this._parameters);
6863 * Tries to set focus on confirm button.
6866 this._dialog.find('button.buttonPrimary').blur().focus();
6871 * Disables the ability to scroll the page.
6873 WCF.System.DisableScrolling = {
6875 * number of times scrolling was disabled (nested calls)
6881 * old overflow-value of the body element
6887 * Disables scrolling.
6889 disable: function () {
6890 // do not block scrolling on touch devices
6891 if ($.browser.touch) {
6895 if (this._depth === 0) {
6896 this._oldOverflow = $(document.body).css('overflow');
6897 $(document.body).css('overflow', 'hidden');
6904 * Enables scrolling again.
6905 * Must be called the same number of times disable() was called to enable scrolling.
6907 enable: function () {
6908 if (this._depth === 0) return;
6912 if (this._depth === 0) {
6913 $(document.body).css('overflow', this._oldOverflow);
6919 * Provides the 'jump to page' overlay.
6921 WCF.System.PageNavigation = {
6929 * page No description
6947 * list of tracked navigation bars
6959 * Initializes the 'jump to page' overlay for given selector.
6961 * @param string selector
6962 * @param object callback
6964 init: function(selector, callback) {
6965 var $elements = $(selector);
6966 if (!$elements.length) {
6970 callback = callback || null;
6971 if (callback !== null && !$.isFunction(callback)) {
6972 console.debug("[WCF
.System
.PageNavigation
] Callback
for selector
'" + selector + "' is invalid
, aborting
.");
6976 this._initElements($elements, callback);
6980 * Initializes the 'jump to page' overlay for given elements.
6982 * @param jQuery elements
6983 * @param object callback
6985 _initElements: function(elements, callback) {
6987 elements.each(function(index, element) {
6988 var $element = $(element);
6989 var $elementID = $element.wcfIdentify();
6991 if (self._elements[$elementID] === undefined) {
6992 self._elements[$elementID] = $element;
6993 $element.find('li.jumpTo').data('elementID', $elementID).click($.proxy(self._click, self));
6995 }).data('callback', callback);
6999 * Shows the 'jump to page' overlay.
7001 * @param object event
7003 _click: function(event) {
7004 this._elementID = $(event.currentTarget).data('elementID');
7006 if (this._dialog === null) {
7007 this._dialog = $('<div id="pageNavigationOverlay
" />').hide().appendTo(document.body);
7009 var $fieldset = $('<fieldset><legend>' + WCF.Language.get('wcf.global.page.jumpTo') + '</legend></fieldset>').appendTo(this._dialog);
7010 $('<dl><dt><label for="jsPageNavigationPageNo
">' + WCF.Language.get('wcf.global.page.jumpTo') + '</label></dt><dd></dd></dl>').appendTo($fieldset);
7011 this._pageNo = $('<input type="number
" id="jsPageNavigationPageNo
" value="1" min="1" max="1" class="tiny
" />').keyup($.proxy(this._keyUp, this)).appendTo($fieldset.find('dd'));
7012 this._description = $('<small></small>').insertAfter(this._pageNo);
7013 var $formSubmit = $('<div class="formSubmit
" />').appendTo(this._dialog);
7014 this._button = $('<button class="buttonPrimary
">' + WCF.Language.get('wcf.global.button.submit') + '</button>').click($.proxy(this._submit, this)).appendTo($formSubmit);
7017 this._button.enable();
7018 this._description.html(WCF.Language.get('wcf.global.page.jumpTo.description').replace(/#pages#/, this._elements[this._elementID].data('pages')));
7019 this._pageNo.val(this._elements[this._elementID].data('pages')).attr('max', this._elements[this._elementID].data('pages'));
7021 this._dialog.wcfDialog({
7022 'title': WCF.Language.get('wcf.global.page.pageNavigation')
7027 * Validates the page No input.
7029 _keyUp: function() {
7030 var $pageNo = parseInt(this._pageNo.val()) || 0;
7031 if ($pageNo < 1 || $pageNo > this._pageNo.attr('max')) {
7032 this._button.disable();
7035 this._button.enable();
7040 * Redirects to given page No.
7042 _submit: function() {
7043 var $pageNavigation = this._elements[this._elementID];
7044 if ($pageNavigation.data('callback') === null) {
7045 var $redirectURL = $pageNavigation.data('link').replace(/pageNo=%d/, 'pageNo=' + this._pageNo.val());
7046 window.location = $redirectURL;
7049 $pageNavigation.data('callback')(this._pageNo.val());
7050 this._dialog.wcfDialog('close');
7056 * Sends periodical requests to protect the session from expiring. By default
7057 * it will send a request 1 minute before it would expire.
7059 * @param integer seconds
7061 WCF.System.KeepAlive = Class.extend({
7063 * Initializes the WCF.System.KeepAlive class.
7065 * @param integer seconds
7067 init: function(seconds) {
7068 new WCF.PeriodicalExecuter(function(pe) {
7069 new WCF.Action.Proxy({
7072 actionName: 'keepAlive',
7073 className: 'wcf\\data\\session\\SessionAction'
7075 failure: function() { pe.stop(); },
7076 showLoadingOverlay: false,
7077 suppressErrors: true
7079 }, (seconds * 1000));
7084 * Default implementation for inline editors.
7086 * @param string elementSelector
7088 WCF.InlineEditor = Class.extend({
7090 * list of registered callbacks
7091 * @var array<object>
7096 * list of dropdown selections
7102 * list of container elements
7108 * notification object
7109 * @var WCF.System.Notification
7111 _notification: null,
7114 * list of known options
7115 * @var array<object>
7121 * @var WCF.Action.Proxy
7126 * list of data to update upon success
7127 * @var array<object>
7132 * Initializes a new inline editor.
7134 init: function(elementSelector) {
7135 var $elements = $(elementSelector);
7136 if (!$elements.length) {
7141 var $quickOption = '';
7142 for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
7143 if (this._options[$i].isQuickOption) {
7144 $quickOption = this._options[$i].optionName;
7150 $elements.each(function(index, element) {
7151 var $element = $(element);
7152 var $elementID = $element.wcfIdentify();
7154 // find trigger element
7155 var $trigger = self._getTriggerElement($element);
7156 if ($trigger === null || $trigger.length !== 1) {
7160 $trigger.click($.proxy(self._show, self)).data('elementID', $elementID);
7162 // simulate click on target action
7163 $trigger.disableSelection().data('optionName', $quickOption).dblclick($.proxy(self._click, self));
7167 self._elements[$elementID] = $element;
7170 this._proxy = new WCF.Action.Proxy({
7171 success: $.proxy(this._success, this)
7174 WCF.CloseOverlayHandler.addCallback('WCF.InlineEditor', $.proxy(this._closeAll, this));
7176 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success');
7180 * Closes all inline editors.
7182 _closeAll: function() {
7183 for (var $elementID in this._elements) {
7184 this._hide($elementID);
7189 * Sets options for this inline editor.
7191 _setOptions: function() {
7192 this._options = [ ];
7196 * Register an option callback for validation and execution.
7198 * @param object callback
7200 registerCallback: function(callback) {
7201 if ($.isFunction(callback)) {
7202 this._callbacks.push(callback);
7207 * Returns the triggering element.
7209 * @param jQuery element
7212 _getTriggerElement: function(element) {
7217 * Shows a dropdown menu if options are available.
7219 * @param object event
7221 _show: function(event) {
7222 var $elementID = $(event.currentTarget).data('elementID');
7225 var $trigger = null;
7226 if (!this._dropdowns[$elementID]) {
7227 $trigger = this._getTriggerElement(this._elements[$elementID]).addClass('dropdownToggle').wrap('<span class="dropdown
" />');
7228 this._dropdowns[$elementID] = $('<ul class="dropdownMenu
" />').insertAfter($trigger);
7230 this._dropdowns[$elementID].empty();
7233 var $hasOptions = false;
7234 var $lastElementType = '';
7235 for (var $i = 0, $length = this._options.length; $i < $length; $i++) {
7236 var $option = this._options[$i];
7238 if ($option.optionName === 'divider') {
7239 if ($lastElementType !== '' && $lastElementType !== 'divider') {
7240 $('<li class="dropdownDivider
" />').appendTo(this._dropdowns[$elementID]);
7241 $lastElementType = $option.optionName;
7244 else if (this._validate($elementID, $option.optionName) || this._validateCallbacks($elementID, $option.optionName)) {
7245 var $listItem = $('<li><span>' + $option.label + '</span></li>').appendTo(this._dropdowns[$elementID]);
7246 $listItem.data('elementID', $elementID).data('optionName', $option.optionName).data('isQuickOption', ($option.isQuickOption ? true : false)).click($.proxy(this._click, this));
7249 $lastElementType = $option.optionName;
7254 // if last child is divider, remove it
7255 var $lastChild = this._dropdowns[$elementID].children().last();
7256 if ($lastChild.hasClass('dropdownDivider')) {
7257 $lastChild.remove();
7260 // check if only element is a quick option
7261 var $quickOption = null;
7263 this._dropdowns[$elementID].children().each(function(index, child) {
7264 var $child = $(child);
7265 if (!$child.hasClass('dropdownDivider')) {
7266 if ($child.data('isQuickOption')) {
7267 $quickOption = $child;
7276 $quickOption.trigger('click');
7278 if ($trigger !== null) {
7279 WCF.Dropdown.close($trigger.parents('.dropdown').wcfIdentify());
7286 if ($trigger !== null) {
7287 WCF.Dropdown.initDropdown($trigger, true);
7294 * Validates an option.
7296 * @param string elementID
7297 * @param string optionName
7300 _validate: function(elementID, optionName) {
7305 * Validates an option provided by callbacks.
7307 * @param string elementID
7308 * @param string optionName
7311 _validateCallbacks: function(elementID, optionName) {
7312 var $length = this._callbacks.length;
7314 for (var $i = 0; $i < $length; $i++) {
7315 if (this._callbacks[$i].validate(this._elements[elementID], optionName)) {
7325 * Handles AJAX responses.
7327 * @param object data
7328 * @param string textStatus
7329 * @param jQuery jqXHR
7331 _success: function(data, textStatus, jqXHR) {
7332 var $length = this._updateData.length;
7337 this._updateState(data);
7339 this._updateData = [ ];
7343 * Update element states based upon update data.
7345 * @param object data
7347 _updateState: function(data) { },
7350 * Handles clicks within dropdown.
7352 * @param object event
7354 _click: function(event) {
7355 var $listItem = $(event.currentTarget);
7356 var $elementID = $listItem.data('elementID');
7357 var $optionName = $listItem.data('optionName');
7359 if (!this._execute($elementID, $optionName)) {
7360 this._executeCallback($elementID, $optionName);
7363 this._hide($elementID);
7367 * Executes actions associated with an option.
7369 * @param string elementID
7370 * @param string optionName
7373 _execute: function(elementID, optionName) {
7378 * Executes actions associated with an option provided by callbacks.
7380 * @param string elementID
7381 * @param string optionName
7384 _executeCallback: function(elementID, optionName) {
7385 var $length = this._callbacks.length;
7387 for (var $i = 0; $i < $length; $i++) {
7388 if (this._callbacks[$i].execute(this._elements[elementID], optionName)) {
7398 * Hides a dropdown menu.
7400 * @param string elementID
7402 _hide: function(elementID) {
7403 if (this._dropdowns[elementID]) {
7404 this._dropdowns[elementID].empty().removeClass('dropdownOpen');
7410 * Default implementation for ajax file uploads
7412 * @param jquery buttonSelector
7413 * @param jquery fileListSelector
7414 * @param string className
7415 * @param jquery options
7417 WCF.Upload = Class.extend({
7419 * name of the upload field
7428 _buttonSelector: null,
7431 * file list selector
7434 _fileListSelector: null,
7449 * iframe for IE<10 fallback
7461 * additional options
7473 * true, if the active user's browser supports ajax file uploads
7476 _supportsAJAXUpload: true,
7479 * fallback overlay for stupid browsers
7485 * Initializes a new upload handler.
7487 * @param string buttonSelector
7488 * @param string fileListSelector
7489 * @param string className
7490 * @param object options
7492 init: function(buttonSelector, fileListSelector, className, options) {
7493 this._buttonSelector = buttonSelector;
7494 this._fileListSelector = fileListSelector;
7495 this._className = className;
7496 this._internalFileID = 0;
7497 this._options = $.extend(true, {
7500 url: 'index.php/AJAXUpload/?t=' + SECURITY_TOKEN + SID_ARG_2ND
7503 // check for ajax upload support
7504 var $xhr = new XMLHttpRequest();
7505 this._supportsAJAXUpload = ($xhr && ('upload' in $xhr) && ('onprogress' in $xhr.upload));
7507 // create upload button
7508 this._createButton();
7512 * Creates the upload button.
7514 _createButton: function() {
7515 if (this._supportsAJAXUpload) {
7516 this._fileUpload = $('<input type="file
" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/>');
7517 this._fileUpload.change($.proxy(this._upload, this));
7518 var $button = $('<p class="button uploadButton
"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>');
7519 $button.prepend(this._fileUpload);
7522 var $button = $('<p class="button uploadFallbackButton
"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>');
7523 $button.click($.proxy(this._showOverlay, this));
7526 this._insertButton($button);
7530 * Inserts the upload button.
7532 * @param jQuery button
7534 _insertButton: function(button) {
7535 this._buttonSelector.append(button);
7539 * Removes the upload button.
7541 _removeButton: function() {
7542 var $selector = '.uploadButton';
7543 if (!this._supportsAJAXUpload) {
7544 $selector = '.uploadFallbackButton';
7547 this._buttonSelector.find($selector).remove();
7551 * Callback for file uploads.
7553 _upload: function() {
7554 var $files = this._fileUpload.prop('files');
7555 if ($files.length) {
7556 var $fd = new FormData();
7557 var $uploadID = this._createUploadMatrix($files);
7559 // no more files left, abort
7560 if (!this._uploadMatrix[$uploadID].length) {
7564 for (var $i = 0, $length = $files.length; $i < $length; $i++) {
7565 if (this._uploadMatrix[$uploadID][$i]) {
7566 var $internalFileID = this._uploadMatrix[$uploadID][$i].data('internalFileID');
7567 $fd.append('__files[' + $internalFileID + ']', $files[$i]);
7571 $fd.append('actionName', this._options.action);
7572 $fd.append('className', this._className);
7573 var $additionalParameters = this._getParameters();
7574 for (var $name in $additionalParameters) {
7575 $fd.append('parameters[' + $name + ']', $additionalParameters[$name]);
7581 url: this._options.url,
7582 enctype: 'multipart/form-data',
7586 success: function(data, textStatus, jqXHR) {
7587 self._success($uploadID, data);
7589 error: $.proxy(this._error, this),
7591 var $xhr = $.ajaxSettings.xhr();
7593 $xhr.upload.addEventListener('progress', function(event) {
7594 self._progress($uploadID, event);
7604 * Creates upload matrix for provided files.
7606 * @param array<object> files
7609 _createUploadMatrix: function(files) {
7611 var $uploadID = this._uploadMatrix.length;
7612 this._uploadMatrix[$uploadID] = [ ];
7614 for (var $i = 0, $length = files.length; $i < $length; $i++) {
7615 var $file = files[$i];
7616 var $li = this._initFile($file);
7618 if (!$li.hasClass('uploadFailed')) {
7619 $li.data('filename', $file.name).data('internalFileID', this._internalFileID++);
7620 this._uploadMatrix[$uploadID][$i] = $li;
7631 * Callback for success event.
7633 * @param integer uploadID
7634 * @param object data
7636 _success: function(uploadID, data) { },
7639 * Callback for error event.
7641 * @param jQuery jqXHR
7642 * @param string textStatus
7643 * @param string errorThrown
7645 _error: function(jqXHR, textStatus, errorThrown) { },
7648 * Callback for progress event.
7650 * @param integer uploadID
7651 * @param object event
7653 _progress: function(uploadID, event) {
7654 var $percentComplete = Math.round(event.loaded * 100 / event.total);
7656 for (var $i in this._uploadMatrix[uploadID]) {
7657 this._uploadMatrix[uploadID][$i].find('progress').attr('value', $percentComplete);
7662 * Returns additional parameters.
7666 _getParameters: function() {
7671 * Initializes list item for uploaded file.
7675 _initFile: function(file) {
7676 return $('<li>' + file.name + ' (' + file.size + ')<progress max="100" /></li>').appendTo(this._fileListSelector);
7680 * Shows the fallback overlay (work in progress)
7682 _showOverlay: function() {
7684 if (this._iframe === null) {
7685 this._iframe = $('<iframe name="__fileUploadIFrame
" />').hide().appendTo(document.body);
7689 if (!this._overlay) {
7690 this._overlay = $('<div><form enctype="multipart
/form-data" method="post" action="' + this._options.url + '" target="__fileUploadIFrame" /></div
>').hide().appendTo(document.body);
7692 var $form = this._overlay.find('form
');
7693 $('<dl
class="wide"><dd
><input type
="file" id
="__fileUpload" name
="' + this._name + '" ' + (this._options.multiple ? 'multiple
="true" ' : '') + '/></dd></dl
>').appendTo($form);
7694 $('<div
class="formSubmit"><input type
="submit" value
="Upload" accesskey
="s" /></div></form
>').appendTo($form);
7696 $('<input type
="hidden" name
="isFallback" value
="1" />').appendTo($form);
7697 $('<input type
="hidden" name
="actionName" value
="' + this._options.action + '" />').appendTo($form);
7698 $('<input type
="hidden" name
="className" value
="' + this._className + '" />').appendTo($form);
7699 var $additionalParameters = this._getParameters();
7700 for (var $name in $additionalParameters) {
7701 $('<input type
="hidden" name
="' + $name + '" value
="' + $additionalParameters[$name] + '" />').appendTo($form);
7704 $form.submit($.proxy(function() {
7706 name: this._getFilename(),
7710 var $uploadID = this._createUploadMatrix([ $file ]);
7712 this._iframe.data('loading
', true).off('load
').load(function() { self._evaluateResponse($uploadID); });
7713 this._overlay.wcfDialog('close
');
7717 this._overlay.wcfDialog({
7718 title: WCF.Language.get('wcf
.global
.button
.upload
')
7723 * Evaluates iframe response.
7725 * @param integer uploadID
7727 _evaluateResponse: function(uploadID) {
7728 var $returnValues = $.parseJSON(this._iframe.contents().find('pre
').html());
7729 this._success(uploadID, $returnValues);
7733 * Returns name of selected file.
7737 _getFilename: function() {
7738 return $('#__fileUpload
').val().split('\\').pop();
7743 * Default implementation for parallel AJAX file uploads.
7745 WCF.Upload.Parallel = WCF.Upload.extend({
7747 * @see WCF.Upload.init()
7749 init: function(buttonSelector, fileListSelector, className, options) {
7750 // force multiple uploads
7751 options = $.extend(true, options || { }, {
7755 this._super(buttonSelector, fileListSelector, className, options);
7759 * @see WCF.Upload._upload()
7761 _upload: function() {
7762 var $files = this._fileUpload.prop('files
');
7763 for (var $i = 0, $length = $files.length; $i < $length; $i++) {
7764 var $file = $files[$i];
7765 var $formData = new FormData();
7766 var $internalFileID = this._createUploadMatrix($file);
7768 if (!this._uploadMatrix[$internalFileID].length) {
7772 $formData.append('__files
[' + $internalFileID + ']', $file);
7773 $formData.append('actionName
', this._options.action);
7774 $formData.append('className
', this._className);
7775 var $additionalParameters = this._getParameters();
7776 for (var $name in $additionalParameters) {
7777 $formData.append('parameters
[' + $name + ']', $additionalParameters[$name]);
7780 this._sendRequest($internalFileID, $formData);
7785 * Sends an AJAX request to upload a file.
7787 * @param integer internalFileID
7788 * @param FormData formData
7790 _sendRequest: function(internalFileID, formData) {
7794 url: this._options.url,
7795 enctype: 'multipart
/form
-data
',
7799 success: function(data, textStatus, jqXHR) {
7800 self._success(internalFileID, data);
7802 error: $.proxy(this._error, this),
7804 var $xhr = $.ajaxSettings.xhr();
7806 $xhr.upload.addEventListener('progress
', function(event) {
7807 self._progress(internalFileID, event);
7816 * Creates upload matrix for provided file and returns its internal file id.
7818 * @param object file
7821 _createUploadMatrix: function(file) {
7822 var $li = this._initFile(file);
7823 if (!$li.hasClass('uploadFailed
')) {
7824 $li.data('filename
', file.name).data('internalFileID
', this._internalFileID);
7825 this._uploadMatrix[this._internalFileID++] = $li;
7827 return this._internalFileID - 1;
7834 * Callback for success event.
7836 * @param integer internalFileID
7837 * @param object data
7839 _success: function(internalFileID, data) { },
7842 * Callback for progress event.
7844 * @param integer internalFileID
7845 * @param object event
7847 _progress: function(internalFileID, event) {
7848 var $percentComplete = Math.round(event.loaded * 100 / event.total);
7850 this._uploadMatrix[internalFileID].find('progress
').attr('value
', $percentComplete);
7854 * @see WCF.Upload._showOverlay()
7856 _showOverlay: function() {
7858 if (this._iframe === null) {
7859 this._iframe = $('<iframe name
="__fileUploadIFrame" />').hide().appendTo(document.body);
7863 if (!this._overlay) {
7864 this._overlay = $('<div
><form enctype
="multipart/form-data" method
="post" action
="' + this._options.url + '" target
="__fileUploadIFrame" /></div
>').hide().appendTo(document.body);
7866 var $form = this._overlay.find('form
');
7867 $('<dl
class="wide"><dd
><input type
="file" id
="__fileUpload" name
="' + this._name + '" ' + (this._options.multiple ? 'multiple
="true" ' : '') + '/></dd></dl
>').appendTo($form);
7868 $('<div
class="formSubmit"><input type
="submit" value
="Upload" accesskey
="s" /></div></form
>').appendTo($form);
7870 $('<input type
="hidden" name
="isFallback" value
="1" />').appendTo($form);
7871 $('<input type
="hidden" name
="actionName" value
="' + this._options.action + '" />').appendTo($form);
7872 $('<input type
="hidden" name
="className" value
="' + this._className + '" />').appendTo($form);
7873 var $additionalParameters = this._getParameters();
7874 for (var $name in $additionalParameters) {
7875 $('<input type
="hidden" name
="' + $name + '" value
="' + $additionalParameters[$name] + '" />').appendTo($form);
7878 $form.submit($.proxy(function() {
7880 name: this._getFilename(),
7884 var $internalFileID = this._createUploadMatrix($file);
7886 this._iframe.data('loading
', true).off('load
').load(function() { self._evaluateResponse($internalFileID); });
7887 this._overlay.wcfDialog('close
');
7891 this._overlay.wcfDialog({
7892 title: WCF.Language.get('wcf
.global
.button
.upload
')
7897 * Evaluates iframe response.
7899 * @param integer internalFileID
7901 _evaluateResponse: function(internalFileID) {
7902 var $returnValues = $.parseJSON(this._iframe.contents().find('pre
').html());
7903 this._success(internalFileID, $returnValues);
7908 * Namespace for sortables.
7913 * Sortable implementation for lists.
7915 * @param string containerID
7916 * @param string className
7917 * @param integer offset
7918 * @param object options
7920 WCF.Sortable.List = Class.extend({
7922 * additional parameters for AJAX request
7925 _additionalParameters: { },
7946 * notification object
7947 * @var WCF.System.Notification
7949 _notification: null,
7965 * @var WCF.Action.Proxy
7976 * Creates a new sortable list.
7978 * @param string containerID
7979 * @param string className
7980 * @param integer offset
7981 * @param object options
7982 * @param boolean isSimpleSorting
7983 * @param object additionalParameters
7985 init: function(containerID, className, offset, options, isSimpleSorting, additionalParameters) {
7986 this._additionalParameters = additionalParameters || { };
7987 this._containerID = $.wcfEscapeID(containerID);
7988 this._container = $('#' + this._containerID);
7989 this._className = className;
7990 this._offset = (offset) ? offset : 0;
7991 this._proxy = new WCF.Action.Proxy({
7992 success: $.proxy(this._success, this)
7994 this._structure = { };
7997 this._options = $.extend(true, {
7999 connectWith: '#' + this._containerID + ' .sortableList
',
8000 disableNesting: 'sortableNoNesting
',
8002 errorClass: 'sortableInvalidTarget
',
8003 forcePlaceholderSize: true,
8005 items: 'li
:not(.sortableNoSorting
)',
8007 placeholder: 'sortablePlaceholder
',
8008 tolerance: 'pointer
',
8009 toleranceElement: '> span
'
8012 if (isSimpleSorting) {
8013 $('#' + this._containerID + ' .sortableList
').sortable(this._options);
8016 $('#' + this._containerID + ' > .sortableList
').nestedSortable(this._options);
8019 if (this._className) {
8020 var $formSubmit = this._container.find('.formSubmit
');
8021 if (!$formSubmit.length) {
8022 $formSubmit = this._container.next('.formSubmit
');
8023 if (!$formSubmit.length) {
8024 console.debug("[WCF.Sortable.Simple] Unable to find form submit for saving, aborting.");
8029 $formSubmit.children('button
[data
-type
="submit"]').click($.proxy(this._submit, this));
8034 * Saves object structure.
8036 _submit: function() {
8038 this._structure = { };
8041 this._container.find('.sortableList
').each($.proxy(function(index, list) {
8042 var $list = $(list);
8043 var $parentID = $list.data('objectID
');
8045 if ($parentID !== undefined) {
8046 $list.children(this._options.items).each($.proxy(function(index, listItem) {
8047 var $objectID = $(listItem).data('objectID
');
8049 if (!this._structure[$parentID]) {
8050 this._structure[$parentID] = [ ];
8053 this._structure[$parentID].push($objectID);
8059 var $parameters = $.extend(true, {
8061 offset: this._offset,
8062 structure: this._structure
8064 }, this._additionalParameters);
8066 this._proxy.setOption('data
', {
8067 actionName: 'updatePosition
',
8068 className: this._className,
8069 interfaceName: 'wcf
\\data
\\ISortableAction
',
8070 parameters: $parameters
8072 this._proxy.sendRequest();
8076 * Shows notification upon success.
8078 * @param object data
8079 * @param string textStatus
8080 * @param jQuery jqXHR
8082 _success: function(data, textStatus, jqXHR) {
8083 if (this._notification === null) {
8084 this._notification = new WCF.System.Notification(WCF.Language.get('wcf
.global
.success
.edit
'));
8087 this._notification.show();
8091 WCF.Popover = Class.extend({
8093 * currently active element id
8096 _activeElementID: '',
8102 _cancelPopover: false,
8111 * default dimensions, should reflect the estimated size
8114 _defaultDimensions: {
8120 * default orientation, may be a combintion of left/right and bottom/top
8123 _defaultOrientation: {
8129 * delay to show or hide popover, values in miliseconds
8138 * true, if an element is being hovered
8141 _hoverElement: false,
8144 * element id of element being hovered
8147 _hoverElementID: '',
8150 * true, if popover is being hovered
8153 _hoverPopover: false,
8156 * minimum margin (all directions) for popover
8162 * periodical executer once element or popover is no longer being hovered
8163 * @var WCF.PeriodicalExecuter
8168 * periodical executer once an element is being hovered
8169 * @var WCF.PeriodicalExecuter
8171 _peOverElement: null,
8183 _popoverContent: null,
8186 * popover horizontal offset
8198 * Initializes a new WCF.Popover object.
8200 * @param string selector
8202 init: function(selector) {
8203 if ($.browser.mobile) return;
8205 // assign default values
8206 this._activeElementID = '';
8207 this._cancelPopover = false;
8209 this._defaultDimensions = {
8213 this._defaultOrientation = {
8221 this._hoverElement = false;
8222 this._hoverElementID = '';
8223 this._hoverPopover = false;
8226 this._peOverElement = null;
8227 this._popoverOffset = 10;
8228 this._selector = selector;
8230 this._popover = $('<div
class="popover"><span
class="icon icon48 icon-spinner"></span><div class="popoverContent"></div></div
>').hide().appendTo(document.body);
8231 this._popoverContent = this._popover.children('.popoverContent
:eq(0)');
8232 this._popover.hover($.proxy(this._overPopover, this), $.proxy(this._out, this));
8234 this._initContainers();
8235 WCF.DOMNodeInsertedHandler.addCallback('WCF
.Popover
.'+selector, $.proxy(this._initContainers, this));
8239 * Initializes all element triggers.
8241 _initContainers: function() {
8242 if ($.browser.mobile) return;
8244 var $elements = $(this._selector);
8245 if (!$elements.length) {
8249 $elements.each($.proxy(function(index, element) {
8250 var $element = $(element);
8251 var $elementID = $element.wcfIdentify();
8253 if (!this._data[$elementID]) {
8254 this._data[$elementID] = {
8259 $element.hover($.proxy(this._overElement, this), $.proxy(this._out, this));
8261 if ($element.is('a
') && $element.attr('href
')) {
8262 $element.click($.proxy(this._cancel, this));
8269 * Cancels popovers if link is being clicked
8271 _cancel: function(event) {
8272 this._cancelPopover = true;
8277 * Triggered once an element is being hovered.
8279 * @param object event
8281 _overElement: function(event) {
8282 if (this._cancelPopover) {
8286 if (this._peOverElement !== null) {
8287 this._peOverElement.stop();
8290 var $elementID = $(event.currentTarget).wcfIdentify();
8291 this._hoverElementID = $elementID;
8292 this._peOverElement = new WCF.PeriodicalExecuter($.proxy(function(pe) {
8295 // still above the same element
8296 if (this._hoverElementID === $elementID) {
8297 this._activeElementID = $elementID;
8300 }, this), this._delay.show);
8302 this._hoverElement = true;
8303 this._hoverPopover = false;
8307 * Prepares popover to be displayed.
8309 _prepare: function() {
8310 if (this._cancelPopover) {
8314 if (this._peOut !== null) {
8319 if (this._popover.is(':visible
')) {
8324 if (!this._data[this._activeElementID].loading && this._data[this._activeElementID].content) {
8325 this._popoverContent.html(this._data[this._activeElementID].content);
8327 WCF.DOMNodeInsertedHandler.execute();
8330 this._data[this._activeElementID].loading = true;
8334 var $dimensions = this._popover.show().getDimensions();
8335 if (this._data[this._activeElementID].loading) {
8337 height: Math.max($dimensions.height, this._defaultDimensions.height),
8338 width: Math.max($dimensions.width, this._defaultDimensions.width)
8342 $dimensions = this._fixElementDimensions(this._popover, $dimensions);
8344 this._popover.hide();
8347 var $orientation = this._getOrientation($dimensions.height, $dimensions.width);
8348 this._popover.css(this._getCSS($orientation.x, $orientation.y));
8350 // apply orientation to popover
8351 this._popover.removeClass('bottom left right top
').addClass($orientation.x).addClass($orientation.y);
8357 * Displays the popover.
8360 if (this._cancelPopover) {
8364 this._popover.stop().show().css({ opacity: 1 }).wcfFadeIn();
8366 if (this._data[this._activeElementID].loading) {
8367 this._popover.children('span
').show();
8368 this._loadContent();
8371 this._popover.children('span
').hide();
8372 this._popoverContent.css({ opacity: 1 });
8377 * Loads content, should be overwritten by child classes.
8379 _loadContent: function() { },
8382 * Inserts content and animating transition.
8384 * @param string elementID
8385 * @param boolean animate
8387 _insertContent: function(elementID, content, animate) {
8388 this._data[elementID] = {
8393 // only update content if element id is active
8394 if (this._activeElementID === elementID) {
8396 // get current dimensions
8397 var $dimensions = this._popoverContent.getDimensions();
8399 // insert new content
8400 this._popoverContent.css({
8404 this._popoverContent.html(this._data[elementID].content);
8405 var $newDimensions = this._popoverContent.getDimensions();
8407 // enforce current dimensions and remove HTML
8408 this._popoverContent.html('').css({
8409 height: $dimensions.height + 'px
',
8410 width: $dimensions.width + 'px
'
8413 // animate to new dimensons
8415 this._popoverContent.animate({
8416 height: $newDimensions.height + 'px
',
8417 width: $newDimensions.width + 'px
'
8418 }, 300, function() {
8419 self._popover.children('span
').hide();
8420 self._popoverContent.html(self._data[elementID].content).css({ opacity: 0 }).animate({ opacity: 1 }, 200);
8422 WCF.DOMNodeInsertedHandler.execute();
8426 // insert new content
8427 this._popover.children('span
').hide();
8428 this._popoverContent.html(this._data[elementID].content);
8430 WCF.DOMNodeInsertedHandler.execute();
8436 * Hides the popover.
8438 _hide: function(disableAnimation) {
8440 this._popoverContent.stop();
8441 this._popover.stop();
8443 if (disableAnimation) {
8444 self._popover.css({ opacity: 0 }).hide();
8445 self._popoverContent.empty().css({ height: 'auto
', opacity: 0, width: 'auto
' });
8448 this._popover.wcfFadeOut(function() {
8449 self._popoverContent.empty().css({ height: 'auto
', opacity: 0, width: 'auto
' });
8450 self._popover.hide();
8456 * Triggered once popover is being hovered.
8458 _overPopover: function() {
8459 if (this._peOut !== null) {
8463 this._hoverElement = false;
8464 this._hoverPopover = true;
8468 * Triggered once element *or* popover is now longer hovered.
8470 _out: function(event) {
8471 if (this._cancelPopover) {
8475 this._hoverElementID = '';
8476 this._hoverElement = false;
8477 this._hoverPopover = false;
8479 this._peOut = new WCF.PeriodicalExecuter($.proxy(function(pe) {
8482 // hide popover is neither element nor popover was hovered given time
8483 if (!this._hoverElement && !this._hoverPopover) {
8486 }, this), this._delay.hide);
8490 * Resolves popover orientation, tries to use default orientation first.
8492 * @param integer height
8493 * @param integer width
8496 _getOrientation: function(height, width) {
8497 // get offsets and dimensions
8498 var $element = $('#' + this._activeElementID);
8499 var $offsets = $element.getOffsets('offset
');
8500 var $elementDimensions = $element.getDimensions();
8501 var $documentDimensions = $(document).getDimensions();
8503 // try default orientation first
8504 var $orientationX = (this._defaultOrientation.x === 'left
') ? 'left
' : 'right
';
8505 var $orientationY = (this._defaultOrientation.y === 'bottom
') ? 'bottom
' : 'top
';
8506 var $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
8508 if ($result.flawed) {
8509 // try flipping orientationX
8510 $orientationX = ($orientationX === 'left
') ? 'right
' : 'left
';
8511 $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
8513 if ($result.flawed) {
8514 // try flipping orientationY while maintaing original orientationX
8515 $orientationX = ($orientationX === 'right
') ? 'left
' : 'right
';
8516 $orientationY = ($orientationY === 'bottom
') ? 'top
' : 'bottom
';
8517 $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
8519 if ($result.flawed) {
8520 // try flipping both orientationX and orientationY compared to default values
8521 $orientationX = ($orientationX === 'left
') ? 'right
' : 'left
';
8522 $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
8524 if ($result.flawed) {
8525 // fuck this shit, we will use the default orientation
8526 $orientationX = (this._defaultOrientationX === 'left
') ? 'left
' : 'right
';
8527 $orientationY = (this._defaultOrientationY === 'bottom
') ? 'bottom
' : 'top
';
8540 * Evaluates if popover fits into given orientation.
8542 * @param string orientationX
8543 * @param string orientationY
8544 * @param object offsets
8545 * @param object elementDimensions
8546 * @param object documentDimensions
8547 * @param integer height
8548 * @param integer width
8551 _evaluateOrientation: function(orientationX, orientationY, offsets, elementDimensions, documentDimensions, height, width) {
8552 var $heightDifference = 0, $widthDifference = 0;
8553 switch (orientationX) {
8555 $widthDifference = offsets.left - width;
8559 $widthDifference = documentDimensions.width - (offsets.left + width);
8563 switch (orientationY) {
8565 $heightDifference = documentDimensions.height - (offsets.top + elementDimensions.height + this._popoverOffset + height);
8569 $heightDifference = offsets.top - (height - this._popoverOffset);
8573 // check if both difference are above margin
8574 var $flawed = false;
8575 if ($heightDifference < this._margin || $widthDifference < this._margin) {
8581 x: $widthDifference,
8582 y: $heightDifference
8587 * Computes CSS for popover.
8589 * @param string orientationX
8590 * @param string orientationY
8593 _getCSS: function(orientationX, orientationY) {
8601 var $element = $('#' + this._activeElementID);
8602 var $offsets = $element.getOffsets('offset
');
8603 var $elementDimensions = this._fixElementDimensions($element, $element.getDimensions());
8604 var $windowDimensions = $(window).getDimensions();
8606 switch (orientationX) {
8608 $css.right = $windowDimensions.width - ($offsets.left + $elementDimensions.width);
8612 $css.left = $offsets.left;
8616 switch (orientationY) {
8618 $css.top = $offsets.top + ($elementDimensions.height + this._popoverOffset);
8622 $css.bottom = $windowDimensions.height - ($offsets.top - this._popoverOffset);
8630 * Tries to fix dimensions if element is partially hidden (overflow: hidden).
8632 * @param jQuery element
8633 * @param object dimensions
8634 * @return dimensions
8636 _fixElementDimensions: function(element, dimensions) {
8637 var $parentDimensions = element.parent().getDimensions();
8639 if ($parentDimensions.height < dimensions.height) {
8640 dimensions.height = $parentDimensions.height;
8643 if ($parentDimensions.width < dimensions.width) {
8644 dimensions.width = $parentDimensions.width;
8652 * Provides an extensible item list with built-in search.
8654 * @param string itemListSelector
8655 * @param string searchInputSelector
8657 WCF.EditableItemList = Class.extend({
8659 * allows custom input not recognized by search to be added
8662 _allowCustomInput: false,
8671 * internal data storage
8683 * item list container
8702 * @var WCF.Search.Base
8707 * search input element
8713 * Creates a new WCF.EditableItemList object.
8715 * @param string itemListSelector
8716 * @param string searchInputSelector
8718 init: function(itemListSelector, searchInputSelector) {
8719 this._itemList = $(itemListSelector);
8720 this._searchInput = $(searchInputSelector);
8723 if (!this._itemList.length || !this._searchInput.length) {
8724 console.debug("[WCF.EditableItemList] Item list and/or search input do not exist, aborting.");
8728 this._objectID = this._getObjectID();
8729 this._objectTypeID = this._getObjectTypeID();
8731 // bind item listener
8732 this._itemList.find('.jsEditableItem
').click($.proxy(this._click, this));
8735 if (!this._itemList.children('ul
').length) {
8736 $('<ul
/>').appendTo(this._itemList);
8738 this._itemList = this._itemList.children('ul
');
8741 this._form = this._itemList.parents('form
').submit($.proxy(this._submit, this));
8743 if (this._allowCustomInput) {
8745 this._searchInput.keydown($.proxy(this._keyDown, this)).on('paste
', function() {
8746 setTimeout(function() { self._onPaste(); }, 100);
8750 // block form submit through [ENTER]
8751 this._searchInput.parents('.dropdown
').data('preventSubmit
', true);
8755 * Handles the key down event.
8757 * @param object event
8759 _keyDown: function(event) {
8761 if (event === null || event.which === 188 || event.which === $.ui.keyCode.ENTER) {
8762 if (event !== null && event.which === $.ui.keyCode.ENTER && this._search) {
8763 if (this._search._itemIndex !== -1) {
8768 var $value = $.trim(this._searchInput.val());
8770 // read everything left from caret position
8771 if (event && event.which === 188) {
8772 $value = $value.substring(0, this._searchInput.getCaret());
8775 if ($value === '') {
8785 if (event && event.which === 188) {
8786 this._searchInput.val($.trim(this._searchInput.val().substr(this._searchInput.getCaret())));
8789 this._searchInput.val('');
8792 if (event !== null) {
8793 event.stopPropagation();
8803 * Handle paste event.
8805 _onPaste: function() {
8806 // split content by comma
8807 var $value = $.trim(this._searchInput.val());
8808 $value = $value.split(',');
8810 for (var $i = 0, $length = $value.length; $i < $length; $i++) {
8811 var $label = $.trim($value[$i]);
8812 if ($label === '') {
8822 this._searchInput.val('');
8826 * Loads raw data and converts it into internal structure. Override this methods
8827 * in your derived classes.
8829 * @param object data
8831 load: function(data) { },
8834 * Removes an item on click.
8836 * @param object event
8839 _click: function(event) {
8840 var $element = $(event.currentTarget);
8841 var $objectID = $element.data('objectID
');
8842 var $label = $element.data('label
');
8845 this._search.removeExcludedSearchValue($label);
8847 this._removeItem($objectID, $label);
8851 event.stopPropagation();
8856 * Returns current object id.
8860 _getObjectID: function() {
8865 * Returns current object type id.
8869 _getObjectTypeID: function() {
8874 * Adds a new item to the list.
8876 * @param object data
8879 addItem: function(data) {
8880 if (this._data[data.objectID]) {
8881 if (!(data.objectID === 0 && this._allowCustomInput)) {
8886 var $listItem = $('<li
class="badge">' + WCF.String.escapeHTML(data.label) + '</li
>').data('objectID
', data.objectID).data('label
', data.label).appendTo(this._itemList);
8887 $listItem.click($.proxy(this._click, this));
8890 this._search.addExcludedSearchValue(data.label);
8892 this._addItem(data.objectID, data.label);
8898 * Clears the list of items.
8900 clearList: function() {
8901 this._itemList.children('li
').each($.proxy(function(index, element) {
8902 var $element = $(element);
8905 this._search.removeExcludedSearchValue($element.data('label
'));
8909 this._removeItem($element.data('objectID
'), $element.data('label
'));
8914 * Handles form submit, override in your class.
8916 _submit: function() {
8917 this._keyDown(null);
8921 * Adds an item to internal storage.
8923 * @param integer objectID
8924 * @param string label
8926 _addItem: function(objectID, label) {
8927 this._data[objectID] = label;
8931 * Removes an item from internal storage.
8933 * @param integer objectID
8934 * @param string label
8936 _removeItem: function(objectID, label) {
8937 delete this._data[objectID];
8941 * Returns the search input field.
8945 getSearchInput: function() {
8946 return this._searchInput;
8951 * Provides a generic sitemap.
8953 WCF.Sitemap = Class.extend({
8955 * sitemap name cache
8967 * initialization state
8974 * @var WCF.Action.Proxy
8979 * Initializes the generic sitemap.
8982 $('#sitemap
').click($.proxy(this._click, this));
8985 this._dialog = null;
8986 this._didInit = false;
8987 this._proxy = new WCF.Action.Proxy({
8988 success: $.proxy(this._success, this)
8993 * Handles clicks on the sitemap icon.
8995 _click: function() {
8996 if (this._dialog === null) {
8997 this._dialog = $('<div id
="sitemapDialog" />').appendTo(document.body);
8999 this._proxy.setOption('data
', {
9000 actionName: 'getSitemap
',
9001 className: 'wcf
\\data
\\sitemap
\\SitemapAction
'
9003 this._proxy.sendRequest();
9006 this._dialog.wcfDialog('open
');
9011 * Handles successful AJAX responses.
9013 * @param object data
9014 * @param string textStatus
9015 * @param jQuery jqXHR
9017 _success: function(data, textStatus, jqXHR) {
9018 if (this._didInit) {
9019 this._cache.push(data.returnValues.sitemapName);
9021 this._dialog.find('#sitemap_
' + data.returnValues.sitemapName).html(data.returnValues.template);
9024 this._dialog.wcfDialog('render
');
9027 // mark sitemap name as loaded
9028 this._cache.push(data.returnValues.sitemapName);
9030 // insert sitemap template
9031 this._dialog.html(data.returnValues.template);
9033 // bind event listener
9034 this._dialog.find('.sitemapNavigation
').click($.proxy(this._navigate, this));
9036 // select active item
9037 this._dialog.find('.tabMenuContainer
').wcfTabs('select
', 'sitemap_
' + data.returnValues.sitemapName);
9040 this._dialog.wcfDialog({
9041 title: WCF.Language.get('wcf
.page
.sitemap
')
9044 this._didInit = true;
9049 * Navigates between different sitemaps.
9051 * @param object event
9053 _navigate: function(event) {
9054 var $sitemapName = $(event.currentTarget).data('sitemapName
');
9055 if (WCF.inArray($sitemapName, this._cache)) {
9056 this._dialog.find('.tabMenuContainer
').wcfTabs('select
', 'sitemap_
' + $sitemapName);
9059 this._dialog.wcfDialog('render
');
9062 this._proxy.setOption('data
', {
9063 actionName: 'getSitemap
',
9064 className: 'wcf
\\data
\\sitemap
\\SitemapAction
',
9066 sitemapName: $sitemapName
9069 this._proxy.sendRequest();
9075 * Provides a language chooser.
9077 * @param string containerID
9078 * @param string inputFieldID
9079 * @param integer languageID
9080 * @param object languages
9081 * @param object callback
9083 WCF.Language.Chooser = Class.extend({
9103 * Initializes the language chooser.
9105 * @param string containerID
9106 * @param string inputFieldID
9107 * @param integer languageID
9108 * @param object languages
9109 * @param object callback
9110 * @param boolean allowEmptyValue
9112 init: function(containerID, inputFieldID, languageID, languages, callback, allowEmptyValue) {
9113 var $container = $('#' + containerID);
9114 if ($container.length != 1) {
9115 console.debug("[WCF.Language.Chooser] Invalid container id '" + containerID + "' given");
9119 // bind language id input
9120 this._input = $('#' + inputFieldID);
9121 if (!this._input.length) {
9122 this._input = $('<input type
="hidden" name
="' + inputFieldID + '" value
="' + languageID + '" />').appendTo($container);
9126 if (callback !== undefined) {
9127 if (!$.isFunction(callback)) {
9128 console.debug("[WCF.Language.Chooser] Given callback is invalid");
9132 this._callback = callback;
9135 // create language dropdown
9136 this._dropdown = $('<div
class="dropdown" id
="' + containerID + '-languageChooser" />').appendTo($container);
9137 $('<div
class="dropdownToggle boxFlag box24" data
-toggle
="' + containerID + '-languageChooser"></div
>').appendTo(this._dropdown);
9138 var $dropdownMenu = $('<ul
class="dropdownMenu" />').appendTo(this._dropdown);
9140 for (var $languageID in languages) {
9141 var $language = languages[$languageID];
9142 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);
9143 $item.data('languageID
', $languageID).click($.proxy(this._click, this));
9145 // update dropdown label
9146 if ($languageID == languageID) {
9147 var $html = $('' + $item.html());
9148 var $innerContent = $html.children().detach();
9149 this._dropdown.children('.dropdownToggle
').empty().append($innerContent);
9153 // allow an empty selection (e.g. using as language filter)
9154 if (allowEmptyValue) {
9155 $('<li
class="dropdownDivider" />').appendTo($dropdownMenu);
9156 var $item = $('<li
><a
>' + WCF.Language.get('wcf
.global
.language
.noSelection
') + '</a></li>').data('languageID
', 0).click($.proxy(this._click, this)).appendTo($dropdownMenu);
9158 if (languageID === 0) {
9159 this._dropdown.children('.dropdownToggle
').empty().append($item.html());
9163 WCF.Dropdown.init();
9167 * Handles click events.
9169 * @param object event
9171 _click: function(event) {
9172 var $item = $(event.currentTarget);
9173 var $languageID = $item.data('languageID
');
9175 // update input field
9176 this._input.val($languageID);
9178 // update dropdown label
9179 var $html = $('' + $item.html());
9180 var $innerContent = ($languageID === 0) ? $html : $html.children().detach();
9181 this._dropdown.children('.dropdownToggle
').empty().append($innerContent);
9184 if (this._callback !== null) {
9185 this._callback($item);
9191 * Namespace for style related classes.
9196 * Provides a visual style chooser.
9198 WCF.Style.Chooser = Class.extend({
9207 * @var WCF.Action.Proxy
9212 * Initializes the style chooser class.
9215 $('<li
class="styleChooser"><a
>' + WCF.Language.get('wcf
.style
.changeStyle
') + '</a></li>').appendTo($('#footerNavigation
> ul
.navigationItems
')).click($.proxy(this._showDialog, this));
9217 this._proxy = new WCF.Action.Proxy({
9218 success: $.proxy(this._success, this)
9223 * Displays the style chooser dialog.
9225 _showDialog: function() {
9226 if (this._dialog === null) {
9227 this._dialog = $('<div id
="styleChooser" />').hide().appendTo(document.body);
9231 this._dialog.wcfDialog({
9232 title: WCF.Language.get('wcf
.style
.changeStyle
')
9238 * Loads the style chooser dialog.
9240 _loadDialog: function() {
9241 this._proxy.setOption('data
', {
9242 actionName: 'getStyleChooser
',
9243 className: 'wcf
\\data
\\style
\\StyleAction
'
9245 this._proxy.sendRequest();
9249 * Handles successful AJAX requests.
9251 * @param object data
9252 * @param string textStatus
9253 * @param jQuery jqXHR
9255 _success: function(data, textStatus, jqXHR) {
9256 if (data.actionName === 'changeStyle
') {
9257 window.location.reload();
9261 this._dialog.html(data.returnValues.template);
9262 this._dialog.find('li
').addClass('pointer
').click($.proxy(this._click, this));
9268 * Changes user style.
9270 * @param object event
9272 _click: function(event) {
9273 this._proxy.setOption('data
', {
9274 actionName: 'changeStyle
',
9275 className: 'wcf
\\data
\\style
\\StyleAction
',
9276 objectIDs: [ $(event.currentTarget).data('styleID
') ]
9278 this._proxy.sendRequest();
9283 * Converts static user panel items into interactive dropdowns.
9285 * @param string containerID
9287 WCF.UserPanel = Class.extend({
9295 * initialization state
9301 * original link element
9307 * language variable name for 'no items
'
9313 * reverts to original link if return values are empty
9316 _revertOnEmpty: true,
9319 * Initialites the WCF.UserPanel class.
9321 * @param string containerID
9323 init: function(containerID) {
9324 this._container = $('#' + containerID);
9325 this._didLoad = false;
9326 this._revertOnEmpty = true;
9328 if (this._container.length != 1) {
9329 console.debug("[WCF.UserPanel] Unable to find container identfied by '" + containerID + "', aborting.");
9337 * Converts link into an interactive dropdown menu.
9339 _convert: function() {
9340 this._container.addClass('dropdown
');
9341 this._link = this._container.children('a
').remove();
9343 var $button = $('<a
class="dropdownToggle">' + this._link.html() + '</a
>').appendTo(this._container).click($.proxy(this._click, this));
9344 var $dropdownMenu = $('<ul
class="dropdownMenu" />').appendTo(this._container);
9345 $('<li
class="jsDropdownPlaceholder"><span
>' + WCF.Language.get('wcf
.global
.loading
') + '</span></li>').appendTo($dropdownMenu);
9347 this._addDefaultItems($dropdownMenu);
9349 this._container.dblclick($.proxy(function() {
9350 window.location = this._link.attr('href
');
9354 WCF.Dropdown.initDropdown($button, false);
9358 * Adds default items to dropdown menu.
9360 * @param jQuery dropdownMenu
9362 _addDefaultItems: function(dropdownMenu) { },
9365 * Adds a dropdown divider.
9367 * @param jQuery dropdownMenu
9369 _addDivider: function(dropdownMenu) {
9370 $('<li
class="dropdownDivider" />').appendTo(dropdownMenu);
9374 * Handles clicks on the dropdown item.
9376 _click: function() {
9377 if (this._didLoad) {
9381 new WCF.Action.Proxy({
9383 data: this._getParameters(),
9384 success: $.proxy(this._success, this)
9387 this._didLoad = true;
9391 * Returns a list of parameters for AJAX request.
9395 _getParameters: function() {
9400 * Handles successful AJAX requests.
9402 * @param object data
9403 * @param string textStatus
9404 * @param jQuery jqXHR
9406 _success: function(data, textStatus, jqXHR) {
9407 var $dropdownMenu = WCF.Dropdown.getDropdownMenu(this._container.wcfIdentify());
9408 $dropdownMenu.children('.jsDropdownPlaceholder
').remove();
9410 if (data.returnValues && data.returnValues.template) {
9411 $('' + data.returnValues.template).prependTo($dropdownMenu);
9414 var $badge = this._container.find('.badge
');
9415 if (!$badge.length) {
9416 $badge = $('<span
class="badge badgeInverse" />').appendTo(this._container.children('.dropdownToggle
'));
9419 $badge.html(data.returnValues.totalCount);
9421 this._after($dropdownMenu);
9424 $('<li
><span
>' + WCF.Language.get(this._noItems) + '</span></li>').prependTo($dropdownMenu);
9427 this._container.find('.badge
').remove();
9432 * Execute actions after the dropdown menu has been populated.
9434 * @param object dropdownMenu
9436 _after: function(dropdownMenu) { }
9440 * WCF implementation for dialogs, based upon ideas by jQuery UI.
9442 $.widget('ui
.wcfDialog
', {
9468 * plain html for title
9480 * dialog visibility state
9493 closeButtonLabel: null,
9494 closeConfirmMessage: null,
9495 closeViaModal: true,
9507 * @see $.widget._createWidget()
9509 _createWidget: function(options, element) {
9510 // ignore script tags
9511 if ($(element).getTagName() === 'script
') {
9512 console.debug("[ui.wcfDialog] Ignored script tag");
9513 this.element = false;
9517 $.Widget.prototype._createWidget.apply(this, arguments);
9521 * Initializes a new dialog.
9524 if (this.options.autoOpen) {
9529 $(window).resize($.proxy(this._resize, this));
9533 * Creates a new dialog instance.
9535 _create: function() {
9536 if (this.options.closeButtonLabel === null) {
9537 this.options.closeButtonLabel = WCF.Language.get('wcf
.global
.button
.close
');
9540 // create dialog container
9541 this._container = $('<div
class="dialogContainer" />').hide().css({ zIndex: this.options.zIndex }).appendTo(document.body);
9542 this._titlebar = $('<header
class="dialogTitlebar" />').hide().appendTo(this._container);
9543 this._title = $('<span
class="dialogTitle" />').hide().appendTo(this._titlebar);
9544 this._closeButton = $('<a
class="dialogCloseButton jsTooltip" title
="' + this.options.closeButtonLabel + '"><span
/></a>').click($.proxy(this.close, this)).hide().appendTo(this._titlebar);
9545 this._content = $('<div
class="dialogContent" />').appendTo(this._container);
9547 this._setOption('title
', this.options.title);
9548 this._setOption('closable
', this.options.closable);
9550 // move target element into content
9551 var $content = this.element.detach();
9552 this._content.html($content);
9554 // create modal view
9555 if (this.options.modal) {
9556 this._overlay = $('#jsWcfDialogOverlay
');
9557 if (!this._overlay.length) {
9558 this._overlay = $('<div id
="jsWcfDialogOverlay" class="dialogOverlay" />').css({ height: '100%', zIndex: 399 }).hide().appendTo(document.body);
9561 if (this.options.closable && this.options.closeViaModal) {
9562 this._overlay.click($.proxy(this.close, this));
9564 $(document).keyup($.proxy(function(event) {
9565 if (event.keyCode && event.keyCode === $.ui.keyCode.ESCAPE) {
9567 event.preventDefault();
9573 WCF.DOMNodeInsertedHandler.execute();
9577 * Sets the given option to the given value.
9578 * See the jQuery UI widget documentation for more.
9580 _setOption: function(key, value) {
9581 this.options[key] = value;
9583 if (key == 'hideTitle
' || key == 'title
') {
9584 if (!this.options.hideTitle && this.options.title != '') {
9585 this._title.html(this.options.title).show();
9587 this._title.html('');
9589 } else if (key == 'closable
' || key == 'closeButtonLabel
') {
9590 if (this.options.closable) {
9591 this._closeButton.attr('title
', this.options.closeButtonLabel).show().find('span
').html(this.options.closeButtonLabel);
9593 WCF.DOMNodeInsertedHandler.execute();
9595 this._closeButton.hide();
9599 if ((!this.options.hideTitle && this.options.title != '') || this.options.closable) {
9600 this._titlebar.show();
9602 this._titlebar.hide();
9609 * Opens this dialog.
9612 // ignore script tags
9613 if (this.element === false) {
9617 if (this.isOpen()) {
9621 if (this._overlay !== null) {
9622 WCF.activeDialogs++;
9624 if (WCF.activeDialogs === 1) {
9625 this._overlay.show();
9630 this._isOpen = true;
9634 * Returns true if dialog is visible.
9638 isOpen: function() {
9639 return this._isOpen;
9643 * Closes this dialog.
9645 * This function can be manually called, even if the dialog is set as not
9646 * closable by the user.
9648 * @param object event
9650 close: function(event) {
9651 if (!this.isOpen()) {
9655 if (this.options.closeConfirmMessage) {
9656 WCF.System.Confirmation.show(this.options.closeConfirmMessage, $.proxy(function(action) {
9657 if (action === 'confirm
') {
9666 if (event !== undefined) {
9667 event.preventDefault();
9672 * Handles dialog closing, should never be called directly.
9674 * @see $.ui.wcfDialog.close()
9676 _close: function() {
9677 this._isOpen = false;
9678 this._container.wcfFadeOut();
9680 if (this._overlay !== null) {
9681 WCF.activeDialogs--;
9683 if (WCF.activeDialogs === 0) {
9684 this._overlay.hide();
9688 if (this.options.onClose !== null) {
9689 this.options.onClose();
9694 * Renders dialog on resize if visible.
9696 _resize: function() {
9697 if (this.isOpen()) {
9703 * Renders this dialog, should be called whenever content is updated.
9705 render: function() {
9706 // check if this if dialog was previously hidden and container is fixed
9707 // at 0px (mobile optimization), in this case scroll to top
9708 if (!this._container.is(':visible
') && this._container.css('top
') === '0px
') {
9709 window.scrollTo(0, 0);
9712 // force dialog and it's contents to be visible
9713 this._container
.show();
9714 this._content
.children().show();
9716 // remove fixed content dimensions for calculation
9722 // terminate concurrent rendering processes
9723 this._container
.stop();
9724 this._content
.stop();
9726 // set dialog to be fully opaque, prevents weird bugs in WebKit
9727 this._container
.show().css('opacity', 1.0);
9729 // handle positioning of form submit controls
9730 var $heightDifference
= 0;
9731 if (this._content
.find('.formSubmit').length
) {
9732 $heightDifference
= this._content
.find('.formSubmit').outerHeight();
9734 this._content
.addClass('dialogForm').css({ marginBottom
: $heightDifference
+ 'px' });
9737 this._content
.removeClass('dialogForm').css({ marginBottom
: '0px' });
9740 // force 800px or 90% width
9741 var $windowDimensions
= $(window
).getDimensions();
9742 if ($windowDimensions
.width
* 0.9 > 800) {
9743 this._container
.css('maxWidth', '800px');
9746 // calculate dimensions
9747 var $containerDimensions
= this._container
.getDimensions('outer');
9748 var $contentDimensions
= this._content
.getDimensions();
9750 // calculate maximum content height
9751 var $heightDifference
= $containerDimensions
.height
- $contentDimensions
.height
;
9752 var $maximumHeight
= $windowDimensions
.height
- $heightDifference
- 120;
9753 this._content
.css({ maxHeight
: $maximumHeight
+ 'px' });
9755 this._determineOverflow();
9757 // calculate new dimensions
9758 $containerDimensions
= this._container
.getDimensions('outer');
9761 var $leftOffset
= Math
.round(($windowDimensions
.width
- $containerDimensions
.width
) / 2);
9762 var $topOffset
= Math
.round(($windowDimensions
.height
- $containerDimensions
.height
) / 2);
9764 // place container at 20% height if possible
9765 var $desiredTopOffset
= Math
.round(($windowDimensions
.height
/ 100) * 20);
9766 if ($desiredTopOffset
< $topOffset
) {
9767 $topOffset
= $desiredTopOffset
;
9771 this._container
.css({
9772 left
: $leftOffset
+ 'px',
9773 top
: $topOffset
+ 'px'
9776 // remove static dimensions
9782 if (!this.isOpen()) {
9783 // hide container again
9784 this._container
.hide();
9786 // fade in container
9787 this._container
.wcfFadeIn($.proxy(function() {
9788 if (this.options
.onShow
!== null) {
9789 this.options
.onShow();
9796 * Determines content overflow based upon static dimensions.
9798 _determineOverflow: function() {
9799 var $max
= $(window
).getDimensions();
9800 var $maxHeight
= this._content
.css('maxHeight');
9801 this._content
.css('maxHeight', 'none');
9802 var $dialog
= this._container
.getDimensions('outer');
9804 var $overflow
= 'visible';
9805 if (($max
.height
* 0.8 < $dialog
.height
) || ($max
.width
* 0.8 < $dialog
.width
)) {
9809 this._content
.css('overflow', $overflow
);
9810 this._content
.css('maxHeight', $maxHeight
);
9812 if ($overflow
=== 'visible') {
9813 // content may already overflow, even though the overall height is still below the threshold
9814 var $contentHeight
= 0;
9815 this._content
.children().each(function(index
, child
) {
9816 $contentHeight
+= $(child
).outerHeight();
9819 if (this._content
.height() < $contentHeight
) {
9820 this._content
.css('overflow', 'auto');
9826 * Returns calculated content dimensions.
9828 * @param integer maximumHeight
9831 _getContentDimensions: function(maximumHeight
) {
9832 var $contentDimensions
= this._content
.getDimensions();
9834 // set height to maximum height if exceeded
9835 if (maximumHeight
&& $contentDimensions
.height
> maximumHeight
) {
9836 $contentDimensions
.height
= maximumHeight
;
9839 return $contentDimensions
;
9844 * Provides a slideshow for lists.
9846 $.widget('ui.wcfSlideshow', {
9848 * button list object
9879 * @var WCF.PeriodicalExecuter
9894 /* enables automatic cycling of items */
9896 /* cycle interval in seconds */
9898 /* gap between items in pixels */
9903 * Creates a new instance of ui.wcfSlideshow.
9905 _create: function() {
9906 this._itemList
= this.element
.children('ul');
9907 this._items
= this._itemList
.children('li');
9908 this._count
= this._items
.length
;
9911 if (this._count
> 1) {
9912 this._initSlideshow();
9917 * Initializes the slideshow.
9919 _initSlideshow: function() {
9920 // calculate item dimensions
9921 var $itemHeight
= $(this._items
.get(0)).outerHeight();
9922 this._items
.addClass('slideshowItem');
9923 this._width
= this.element
.css('height', $itemHeight
).innerWidth();
9924 this._itemList
.addClass('slideshowItemList').css('left', 0);
9926 this._items
.each($.proxy(function(index
, item
) {
9927 $(item
).show().css({
9928 height
: $itemHeight
,
9929 left
: ((this._width
+ this.options
.itemGap
) * index
),
9935 height
: $itemHeight
,
9937 }).hover($.proxy(this._hoverIn
, this), $.proxy(this._hoverOut
, this));
9939 // create toggle buttons
9940 this._buttonList
= $('<ul class="slideshowButtonList" />').appendTo(this.element
);
9941 for (var $i
= 0; $i
< this._count
; $i
++) {
9942 var $link
= $('<li><a><span class="icon icon16 icon-circle" /></a></li>').data('index', $i
).click($.proxy(this._click
, this)).appendTo(this._buttonList
);
9944 $link
.find('.icon').addClass('active');
9950 $(window
).resize($.proxy(this._resize
, this));
9954 * Handles browser resizing
9956 _resize: function() {
9957 this._width
= this.element
.css('width', 'auto').innerWidth();
9958 this._items
.each($.proxy(function(index
, item
) {
9960 left
: ((this._width
+ this.options
.itemGap
) * index
),
9970 * Disables cycling while hovering.
9972 _hoverIn: function() {
9973 if (this._timer
!== null) {
9979 * Enables cycling after mouse out.
9981 _hoverOut: function() {
9986 * Resets cycle timer.
9988 _resetTimer: function() {
9989 if (!this.options
.cycle
) {
9993 if (this._timer
!== null) {
9998 this._timer
= new WCF
.PeriodicalExecuter(function() {
10000 }, this.options
.cycleInterval
* 1000);
10004 * Handles clicks on the select buttons.
10006 * @param object event
10008 _click: function(event
) {
10009 this.moveTo($(event
.currentTarget
).data('index'));
10011 this._resetTimer();
10015 * Moves to a specified item index, NULL will move to the next item in list.
10017 * @param integer index
10019 moveTo: function(index
) {
10020 this._index
= (index
=== null) ? this._index
+ 1 : index
;
10021 if (this._index
== this._count
) {
10025 $(this._buttonList
.find('.icon').removeClass('active').get(this._index
)).addClass('active');
10026 this._itemList
.css('left', this._index
* (this._width
+ this.options
.itemGap
) * -1);
10028 this._trigger('moveTo', null, { index
: this._index
});
10032 * Returns item by index or null if index is invalid.
10036 getItem: function(index
) {
10037 if (this._items
[index
]) {
10038 return this._items
[index
];
10046 * Custom tab menu implementation for WCF.
10048 $.widget('ui.wcfTabs', $.ui
.tabs
, {
10050 * Workaround for ids containing a dot ".", until jQuery UI devs learn
10051 * to properly escape ids ... (it took 18 months until they finally
10054 * @see http://bugs.jqueryui.com/ticket/4681
10055 * @see $.ui.tabs.prototype._sanitizeSelector()
10057 _sanitizeSelector: function(hash
) {
10058 return hash
.replace(/([:\.])/g, '\\$1');
10062 * @see $.ui.tabs.prototype.select()
10064 select: function(index
) {
10065 if (!$.isNumeric(index
)) {
10066 // panel identifier given
10067 this.panels
.each(function(i
, panel
) {
10068 if ($(panel
).wcfIdentify() === index
) {
10074 // unable to identify panel
10075 if (!$.isNumeric(index
)) {
10076 console
.debug("[ui.wcfTabs] Unable to find panel identified by '" + index
+ "', aborting.");
10081 this._setOption('active', index
);
10085 * Selects a specific tab by triggering the 'click' event.
10087 * @param string tabIdentifier
10089 selectTab: function(tabIdentifier
) {
10090 tabIdentifier
= '#' + tabIdentifier
;
10092 this.anchors
.each(function(index
, anchor
) {
10093 var $anchor
= $(anchor
);
10094 if ($anchor
.prop('hash') === tabIdentifier
) {
10095 $anchor
.trigger('click');
10102 * Returns the currently selected tab index.
10106 getCurrentIndex: function() {
10107 return this.lis
.index(this.lis
.filter('.ui-tabs-selected'));
10111 * Returns true if identifier is used by an anchor.
10113 * @param string identifier
10114 * @param boolean isChildren
10117 hasAnchor: function(identifier
, isChildren
) {
10118 var $matches
= false;
10120 this.anchors
.each(function(index
, anchor
) {
10121 var $href
= $(anchor
).attr('href');
10122 if (/#.+/.test($href
)) {
10124 var $parts
= $href
.split('#', 2);
10126 $parts
= $parts
[1].split('-', 2);
10129 if ($parts
[1] === identifier
) {
10142 * Shows default tab.
10144 revertToDefault: function() {
10145 var $active
= this.element
.data('active');
10146 if (!$active
|| $active
=== '') $active
= 0;
10148 this.select($active
);
10152 * @see $.ui.tabs.prototype._processTabs()
10154 _processTabs: function() {
10157 this.tablist
= this._getList()
10158 .addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
10159 .attr( "role", "tablist" );
10161 this.tabs
= this.tablist
.find( "> li:has(a[href])" )
10162 .addClass( "ui-state-default ui-corner-top" )
10168 this.anchors
= this.tabs
.map(function() {
10169 return $( "a", this )[ 0 ];
10171 .addClass( "ui-tabs-anchor" )
10173 role
: "presentation",
10179 this.anchors
.each(function( i
, anchor
) {
10180 var selector
, panel
,
10181 anchorId
= $( anchor
).uniqueId().attr( "id" ),
10182 tab
= $( anchor
).closest( "li" ),
10183 originalAriaControls
= tab
.attr( "aria-controls" );
10186 selector
= anchor
.hash
;
10187 panel
= that
.element
.find( that
._sanitizeSelector( selector
) );
10189 if ( panel
.length
) {
10190 that
.panels
= that
.panels
.add( panel
);
10192 if ( originalAriaControls
) {
10193 tab
.data( "ui-tabs-aria-controls", originalAriaControls
);
10196 "aria-controls": selector
.substring( 1 ),
10197 "aria-labelledby": anchorId
10199 panel
.attr( "aria-labelledby", anchorId
);
10203 .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
10204 .attr( "role", "tabpanel" );
10208 * @see $.ui.tabs.prototype.load()
10210 load: function( index
, event
) {
10216 * jQuery widget implementation of the wcf pagination.
10218 $.widget('ui.wcfPages', {
10220 SHOW_SUB_LINKS
: 20,
10228 // we use options here instead of language variables, because the paginator is not only usable with pages
10234 * Creates the pages widget.
10236 _create: function() {
10237 if (this.options
.nextPage
=== null) this.options
.nextPage
= WCF
.Language
.get('wcf.global.page.next');
10238 if (this.options
.previousPage
=== null) this.options
.previousPage
= WCF
.Language
.get('wcf.global.page.previous');
10240 this.element
.addClass('pageNavigation');
10246 * Destroys the pages widget.
10248 destroy: function() {
10249 $.Widget
.prototype.destroy
.apply(this, arguments
);
10251 this.element
.children().remove();
10255 * Renders the pages widget.
10257 _render: function() {
10258 // only render if we have more than 1 page
10259 if (!this.options
.disabled
&& this.options
.maxPage
> 1) {
10260 var $hasHiddenPages
= false;
10262 // make sure pagination is visible
10263 if (this.element
.hasClass('hidden')) {
10264 this.element
.removeClass('hidden');
10266 this.element
.show();
10268 this.element
.children().remove();
10270 var $pageList
= $('<ul />');
10271 this.element
.append($pageList
);
10273 var $previousElement
= $('<li class="button skip" />');
10274 $pageList
.append($previousElement
);
10276 if (this.options
.activePage
> 1) {
10277 var $previousLink
= $('<a' + ((this.options
.previousPage
!= null) ? (' title="' + this.options
.previousPage
+ '"') : ('')) + '></a>');
10278 $previousElement
.append($previousLink
);
10279 this._bindSwitchPage($previousLink
, this.options
.activePage
- 1);
10281 var $previousImage
= $('<span class="icon icon16 icon-double-angle-left" />');
10282 $previousLink
.append($previousImage
);
10285 var $previousImage
= $('<span class="icon icon16 icon-double-angle-left" />');
10286 $previousElement
.append($previousImage
);
10287 $previousElement
.addClass('disabled').removeClass('button');
10288 $previousImage
.addClass('disabled');
10292 $pageList
.append(this._renderLink(1));
10294 // calculate page links
10295 var $maxLinks
= this.SHOW_LINKS
- 4;
10296 var $linksBefore
= this.options
.activePage
- 2;
10297 if ($linksBefore
< 0) $linksBefore
= 0;
10298 var $linksAfter
= this.options
.maxPage
- (this.options
.activePage
+ 1);
10299 if ($linksAfter
< 0) $linksAfter
= 0;
10300 if (this.options
.activePage
> 1 && this.options
.activePage
< this.options
.maxPage
) $maxLinks
--;
10302 var $half
= $maxLinks
/ 2;
10303 var $left
= this.options
.activePage
;
10304 var $right
= this.options
.activePage
;
10305 if ($left
< 1) $left
= 1;
10306 if ($right
< 1) $right
= 1;
10307 if ($right
> this.options
.maxPage
- 1) $right
= this.options
.maxPage
- 1;
10309 if ($linksBefore
>= $half
) {
10313 $left
-= $linksBefore
;
10314 $right
+= $half
- $linksBefore
;
10317 if ($linksAfter
>= $half
) {
10321 $right
+= $linksAfter
;
10322 $left
-= $half
- $linksAfter
;
10325 $right
= Math
.ceil($right
);
10326 $left
= Math
.ceil($left
);
10327 if ($left
< 1) $left
= 1;
10328 if ($right
> this.options
.maxPage
) $right
= this.options
.maxPage
;
10332 if ($left
- 1 < 2) {
10333 $pageList
.append(this._renderLink(2));
10336 $('<li class="button jumpTo"><a title="' + WCF
.Language
.get('wcf.global.page.jumpTo') + '" class="jsTooltip">...</a></li>').appendTo($pageList
);
10337 $hasHiddenPages
= true;
10342 for (var $i
= $left
+ 1; $i
< $right
; $i
++) {
10343 $pageList
.append(this._renderLink($i
));
10347 if ($right
< this.options
.maxPage
) {
10348 if (this.options
.maxPage
- $right
< 2) {
10349 $pageList
.append(this._renderLink(this.options
.maxPage
- 1));
10352 $('<li class="button jumpTo"><a title="' + WCF
.Language
.get('wcf.global.page.jumpTo') + '" class="jsTooltip">...</a></li>').appendTo($pageList
);
10353 $hasHiddenPages
= true;
10358 $pageList
.append(this._renderLink(this.options
.maxPage
));
10361 var $nextElement
= $('<li class="button skip" />');
10362 $pageList
.append($nextElement
);
10364 if (this.options
.activePage
< this.options
.maxPage
) {
10365 var $nextLink
= $('<a' + ((this.options
.nextPage
!= null) ? (' title="' + this.options
.nextPage
+ '"') : ('')) + '></a>');
10366 $nextElement
.append($nextLink
);
10367 this._bindSwitchPage($nextLink
, this.options
.activePage
+ 1);
10369 var $nextImage
= $('<span class="icon icon16 icon-double-angle-right" />');
10370 $nextLink
.append($nextImage
);
10373 var $nextImage
= $('<span class="icon icon16 icon-double-angle-right" />');
10374 $nextElement
.append($nextImage
);
10375 $nextElement
.addClass('disabled').removeClass('button');
10376 $nextImage
.addClass('disabled');
10379 if ($hasHiddenPages
) {
10380 $pageList
.data('pages', this.options
.maxPage
);
10381 WCF
.System
.PageNavigation
.init('#' + $pageList
.wcfIdentify(), $.proxy(function(pageNo
) {
10382 this.switchPage(pageNo
);
10387 // otherwise hide the paginator if not already hidden
10388 this.element
.hide();
10393 * Renders a page link.
10395 * @parameter integer page
10398 _renderLink: function(page
, lineBreak
) {
10399 var $pageElement
= $('<li class="button"></li>');
10400 if (lineBreak
!= undefined && lineBreak
) {
10401 $pageElement
.addClass('break');
10403 if (page
!= this.options
.activePage
) {
10404 var $pageLink
= $('<a>' + WCF
.String
.addThousandsSeparator(page
) + '</a>');
10405 $pageElement
.append($pageLink
);
10406 this._bindSwitchPage($pageLink
, page
);
10409 $pageElement
.addClass('active');
10410 var $pageSubElement
= $('<span>' + WCF
.String
.addThousandsSeparator(page
) + '</span>');
10411 $pageElement
.append($pageSubElement
);
10414 return $pageElement
;
10418 * Binds the 'click'-event for the page switching to the given element.
10420 * @parameter $(element) element
10421 * @paremeter integer page
10423 _bindSwitchPage: function(element
, page
) {
10425 element
.click(function() {
10426 $self
.switchPage(page
);
10431 * Switches to the given page
10433 * @parameter Event event
10434 * @parameter integer page
10436 switchPage: function(page
) {
10437 this._setOption('activePage', page
);
10441 * Sets the given option to the given value.
10442 * See the jQuery UI widget documentation for more.
10444 _setOption: function(key
, value
) {
10445 if (key
== 'activePage') {
10446 if (value
!= this.options
[key
] && value
> 0 && value
<= this.options
.maxPage
) {
10447 // you can prevent the page switching by returning false or by event.preventDefault()
10448 // in a shouldSwitch-callback. e.g. if an AJAX request is already running.
10449 var $result
= this._trigger('shouldSwitch', undefined, {
10453 if ($result
|| $result
!== undefined) {
10454 this.options
[key
] = value
;
10456 this._trigger('switched', undefined, {
10461 this._trigger('notSwitched', undefined, {
10468 this.options
[key
] = value
;
10470 if (key
== 'disabled') {
10472 this.element
.children().remove();
10478 else if (key
== 'maxPage') {
10487 * Start input of pagenumber
10489 * @parameter Event event
10491 _startInput: function(event
) {
10493 var $childLink
= $(event
.currentTarget
);
10494 if (!$childLink
.is('a')) $childLink
= $childLink
.parent('a');
10499 var $childInput
= $childLink
.parent('li').children('input')
10500 .css('display', 'block')
10503 $childInput
.focus();
10507 * Stops input of pagenumber
10509 * @parameter Event event
10511 _stopInput: function(event
) {
10513 var $childInput
= $(event
.currentTarget
);
10514 $childInput
.css('display', 'none');
10517 var $childContainer
= $childInput
.parent('li');
10518 if ($childContainer
!= undefined && $childContainer
!= null) {
10519 $childContainer
.children('a').show();
10524 * Handles input of pagenumber
10526 * @parameter Event event
10528 _handleInput: function(event
) {
10529 var $ie7
= ($.browser
.msie
&& $.browser
.version
== '7.0');
10530 if (event
.type
!= 'keyup' || $ie7
) {
10531 if (!$ie7
|| ((event
.which
== 13 || event
.which
== 27) && event
.type
== 'keyup')) {
10532 if (event
.which
== 13) {
10533 this.switchPage(parseInt($(event
.currentTarget
).val()));
10536 if (event
.which
== 13 || event
.which
== 27) {
10537 this._stopInput(event
);
10538 event
.stopPropagation();
10546 * Namespace for category related classes.
10548 WCF
.Category
= { };
10551 * Handles selection of categories.
10553 WCF
.Category
.NestedList
= Class
.extend({
10555 * list of categories
10561 * Initializes the WCF.Category.NestedList object.
10565 $('.jsCategory').each(function(index
, category
) {
10566 var $category
= $(category
).data('parentCategoryID', null).change($.proxy(self
._updateSelection
, self
));
10567 self
._categories
[$category
.val()] = $category
;
10569 // find child categories
10570 var $childCategoryIDs
= [ ];
10571 $category
.parents('li').find('.jsChildCategory').each(function(innerIndex
, childCategory
) {
10572 var $childCategory
= $(childCategory
).data('parentCategoryID', $category
.val()).change($.proxy(self
._updateSelection
, self
));
10573 self
._categories
[$childCategory
.val()] = $childCategory
;
10574 $childCategoryIDs
.push($childCategory
.val());
10576 if ($childCategory
.is(':checked')) {
10577 $category
.prop('checked', 'checked');
10581 $category
.data('childCategoryIDs', $childCategoryIDs
);
10586 * Updates selection of categories.
10588 * @param object event
10590 _updateSelection: function(event
) {
10591 var $category
= $(event
.currentTarget
);
10592 var $parentCategoryID
= $category
.data('parentCategoryID');
10594 if ($category
.is(':checked')) {
10596 if ($parentCategoryID
!== null) {
10597 // mark parent category as checked
10598 this._categories
[$parentCategoryID
].prop('checked', 'checked');
10602 // top-level category
10603 if ($parentCategoryID
=== null) {
10604 // unmark all child categories
10605 var $childCategoryIDs
= $category
.data('childCategoryIDs');
10606 for (var $i
= 0, $length
= $childCategoryIDs
.length
; $i
< $length
; $i
++) {
10607 this._categories
[$childCategoryIDs
[$i
]].prop('checked', false);
10615 * Encapsulate eval() within an own function to prevent problems
10616 * with optimizing and minifiny JS.
10618 * @param mixed expression
10621 function wcfEval(expression
) {
10622 return eval(expression
);