Commit | Line | Data |
---|---|---|
158bd3ca | 1 | /** |
e919d1d3 | 2 | * Class and function collection for WCF. |
158bd3ca | 3 | * |
95d8093a AE |
4 | * Major Contributors: Markus Bartz, Tim Duesterhus, Matthias Schmidt and Marcel Werk |
5 | * | |
6 | * @author Alexander Ebert | |
ca4ba303 | 7 | * @copyright 2001-2014 WoltLab GmbH |
158bd3ca TD |
8 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
9 | */ | |
10 | ||
e93829d0 AE |
11 | (function() { |
12 | // store original implementation | |
13 | var $jQueryData = jQuery.fn.data; | |
9f959ced | 14 | |
e93829d0 AE |
15 | /** |
16 | * Override jQuery.fn.data() to support custom 'ID' suffix which will | |
17 | * be translated to '-id' at runtime. | |
18 | * | |
19 | * @see jQuery.fn.data() | |
20 | */ | |
21 | jQuery.fn.data = function(key, value) { | |
1f4f33dd AE |
22 | if (key) { |
23 | switch (typeof key) { | |
24 | case 'object': | |
25 | for (var $key in key) { | |
26 | if ($key.match(/ID$/)) { | |
27 | var $value = key[$key]; | |
28 | delete key[$key]; | |
29 | ||
30 | $key = $key.replace(/ID$/, '-id'); | |
31 | key[$key] = $value; | |
32 | } | |
33 | } | |
34 | ||
35 | arguments[0] = key; | |
36 | break; | |
37 | ||
38 | case 'string': | |
39 | if (key.match(/ID$/)) { | |
40 | arguments[0] = key.replace(/ID$/, '-id'); | |
41 | } | |
42 | break; | |
43 | } | |
e93829d0 | 44 | } |
9f959ced | 45 | |
e93829d0 | 46 | // call jQuery's own data method |
1c93e6e7 AE |
47 | var $data = $jQueryData.apply(this, arguments); |
48 | ||
49 | // handle .data() call without arguments | |
50 | if (key === undefined) { | |
51 | for (var $key in $data) { | |
52 | if ($key.match(/Id$/)) { | |
696930b9 | 53 | $data[$key.replace(/Id$/, 'ID')] = $data[$key]; |
1c93e6e7 AE |
54 | delete $data[$key]; |
55 | } | |
56 | } | |
57 | } | |
58 | ||
59 | return $data; | |
f4126129 | 60 | }; |
e919d1d3 | 61 | |
0af61800 TD |
62 | // provide a sane window.console implementation |
63 | if (!window.console) window.console = { }; | |
64 | var consoleProperties = [ "log",/* "debug",*/ "info", "warn", "exception", "assert", "dir", "dirxml", "trace", "group", "groupEnd", "groupCollapsed", "profile", "profileEnd", "count", "clear", "time", "timeEnd", "timeStamp", "table", "error" ]; | |
65 | for (var i = 0; i < consoleProperties.length; i++) { | |
66 | if (typeof (console[consoleProperties[i]]) === 'undefined') { | |
89be55b6 | 67 | console[consoleProperties[i]] = function () { }; |
0af61800 | 68 | } |
e919d1d3 | 69 | } |
0af61800 TD |
70 | |
71 | if (typeof(console.debug) === 'undefined') { | |
e919d1d3 AE |
72 | // forward console.debug to console.log (IE9) |
73 | console.debug = function(string) { console.log(string); }; | |
74 | } | |
e93829d0 AE |
75 | })(); |
76 | ||
9f959ced MS |
77 | /** |
78 | * Simple JavaScript Inheritance | |
878d0d80 AE |
79 | * By John Resig http://ejohn.org/ |
80 | * MIT Licensed. | |
81 | */ | |
82 | // Inspired by base2 and Prototype | |
f4126129 | 83 | (function(){var a=false,b=/xyz/.test(function(){xyz})?/\b_super\b/:/.*/;this.Class=function(){};Class.extend=function(c){function g(){if(!a&&this.init)this.init.apply(this,arguments);}var d=this.prototype;a=true;var e=new this;a=false;for(var f in c){e[f]=typeof c[f]=="function"&&typeof d[f]=="function"&&b.test(c[f])?function(a,b){return function(){var c=this._super;this._super=d[a];var e=b.apply(this,arguments);this._super=c;return e;};}(f,c[f]):c[f]}g.prototype=e;g.prototype.constructor=g;g.extend=arguments.callee;return g;};})(); |
878d0d80 | 84 | |
f0ef56c5 AE |
85 | /*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas, David Knight. Dual MIT/BSD license */ |
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"}}}()); | |
87 | ||
88 | /*! matchMedia() polyfill addListener/removeListener extension. Author & copyright (c) 2012: Scott Jehl. Dual MIT/BSD license */ | |
89 | (function(){if(window.matchMedia&&window.matchMedia("all").addListener){return false}var e=window.matchMedia,t=e("only all").matches,n=false,r=0,i=[],s=function(t){clearTimeout(r);r=setTimeout(function(){for(var t=0,n=i.length;t<n;t++){var r=i[t].mql,s=i[t].listeners||[],o=e(r.media).matches;if(o!==r.matches){r.matches=o;for(var u=0,a=s.length;u<a;u++){s[u].call(window,r)}}}},30)};window.matchMedia=function(r){var o=e(r),u=[],a=0;o.addListener=function(e){if(!t){return}if(!n){n=true;window.addEventListener("resize",s,true)}if(a===0){a=i.push({mql:o,listeners:u})}u.push(e)};o.removeListener=function(e){for(var t=0,n=u.length;t<n;t++){if(u[t]===e){u.splice(t,1)}}};return o}})(); | |
90 | ||
1a65be7c AE |
91 | /*! |
92 | * enquire.js v2.1.0 - Awesome Media Queries in JavaScript | |
93 | * Copyright (c) 2013 Nick Williams - http://wicky.nillia.ms/enquire.js | |
94 | * License: MIT (http://www.opensource.org/licenses/mit-license.php) | |
95 | */ | |
47516fcd | 96 | (function(t,i,n){var e=i.matchMedia;"undefined"!=typeof module&&module.exports?module.exports=n(e):"function"==typeof define&&define.amd?define(function(){return i[t]=n(e)}):i[t]=n(e)})("enquire",this,function(t){"use strict";function i(t,i){var n,e=0,s=t.length;for(e;s>e&&(n=i(t[e],e),n!==!1);e++);}function n(t){return"[object Array]"===Object.prototype.toString.apply(t)}function e(t){return"function"==typeof t}function s(t){this.options=t,!t.deferSetup&&this.setup()}function o(i,n){this.query=i,this.isUnconditional=n,this.handlers=[],this.mql=t(i);var e=this;this.listener=function(t){e.mql=t,e.assess()},this.mql.addListener(this.listener)}function r(){if(!t)throw Error("matchMedia not present, legacy browsers require a polyfill");this.queries={},this.browserIsIncapable=!t("only all").matches}return s.prototype={setup:function(){this.options.setup&&this.options.setup(),this.initialised=!0},on:function(){!this.initialised&&this.setup(),this.options.match&&this.options.match()},off:function(){this.options.unmatch&&this.options.unmatch()},destroy:function(){this.options.destroy?this.options.destroy():this.off()},equals:function(t){return this.options===t||this.options.match===t}},o.prototype={addHandler:function(t){var i=new s(t);this.handlers.push(i),this.matches()&&i.on()},removeHandler:function(t){var n=this.handlers;i(n,function(i,e){return i.equals(t)?(i.destroy(),!n.splice(e,1)):void 0})},matches:function(){return this.mql.matches||this.isUnconditional},clear:function(){i(this.handlers,function(t){t.destroy()}),this.mql.removeListener(this.listener),this.handlers.length=0},assess:function(){var t=this.matches()?"on":"off";i(this.handlers,function(i){i[t]()})}},r.prototype={register:function(t,s,r){var h=this.queries,u=r&&this.browserIsIncapable;return h[t]||(h[t]=new o(t,u)),e(s)&&(s={match:s}),n(s)||(s=[s]),i(s,function(i){h[t].addHandler(i)}),this},unregister:function(t,i){var n=this.queries[t];return n&&(i?n.removeHandler(i):(n.clear(),delete this.queries[t])),this}},new r}); |
1a65be7c | 97 | |
efce8a94 AE |
98 | /*! head.load - v1.0.3 */ |
99 | (function(n,t){"use strict";function w(){}function u(n,t){if(n){typeof n=="object"&&(n=[].slice.call(n));for(var i=0,r=n.length;i<r;i++)t.call(n,n[i],i)}}function it(n,i){var r=Object.prototype.toString.call(i).slice(8,-1);return i!==t&&i!==null&&r===n}function s(n){return it("Function",n)}function a(n){return it("Array",n)}function et(n){var i=n.split("/"),t=i[i.length-1],r=t.indexOf("?");return r!==-1?t.substring(0,r):t}function f(n){(n=n||w,n._done)||(n(),n._done=1)}function ot(n,t,r,u){var f=typeof n=="object"?n:{test:n,success:!t?!1:a(t)?t:[t],failure:!r?!1:a(r)?r:[r],callback:u||w},e=!!f.test;return e&&!!f.success?(f.success.push(f.callback),i.load.apply(null,f.success)):e||!f.failure?u():(f.failure.push(f.callback),i.load.apply(null,f.failure)),i}function v(n){var t={},i,r;if(typeof n=="object")for(i in n)!n[i]||(t={name:i,url:n[i]});else t={name:et(n),url:n};return(r=c[t.name],r&&r.url===t.url)?r:(c[t.name]=t,t)}function y(n){n=n||c;for(var t in n)if(n.hasOwnProperty(t)&&n[t].state!==l)return!1;return!0}function st(n){n.state=ft;u(n.onpreload,function(n){n.call()})}function ht(n){n.state===t&&(n.state=nt,n.onpreload=[],rt({url:n.url,type:"cache"},function(){st(n)}))}function ct(){var n=arguments,t=n[n.length-1],r=[].slice.call(n,1),f=r[0];return(s(t)||(t=null),a(n[0]))?(n[0].push(t),i.load.apply(null,n[0]),i):(f?(u(r,function(n){s(n)||!n||ht(v(n))}),b(v(n[0]),s(f)?f:function(){i.load.apply(null,r)})):b(v(n[0])),i)}function lt(){var n=arguments,t=n[n.length-1],r={};return(s(t)||(t=null),a(n[0]))?(n[0].push(t),i.load.apply(null,n[0]),i):(u(n,function(n){n!==t&&(n=v(n),r[n.name]=n)}),u(n,function(n){n!==t&&(n=v(n),b(n,function(){y(r)&&f(t)}))}),i)}function b(n,t){if(t=t||w,n.state===l){t();return}if(n.state===tt){i.ready(n.name,t);return}if(n.state===nt){n.onpreload.push(function(){b(n,t)});return}n.state=tt;rt(n,function(){n.state=l;t();u(h[n.name],function(n){f(n)});o&&y()&&u(h.ALL,function(n){f(n)})})}function at(n){n=n||"";var t=n.split("?")[0].split(".");return t[t.length-1].toLowerCase()}function rt(t,i){function e(t){t=t||n.event;u.onload=u.onreadystatechange=u.onerror=null;i()}function o(f){f=f||n.event;(f.type==="load"||/loaded|complete/.test(u.readyState)&&(!r.documentMode||r.documentMode<9))&&(n.clearTimeout(t.errorTimeout),n.clearTimeout(t.cssTimeout),u.onload=u.onreadystatechange=u.onerror=null,i())}function s(){if(t.state!==l&&t.cssRetries<=20){for(var i=0,f=r.styleSheets.length;i<f;i++)if(r.styleSheets[i].href===u.href){o({type:"load"});return}t.cssRetries++;t.cssTimeout=n.setTimeout(s,250)}}var u,h,f;i=i||w;h=at(t.url);h==="css"?(u=r.createElement("link"),u.type="text/"+(t.type||"css"),u.rel="stylesheet",u.href=t.url,t.cssRetries=0,t.cssTimeout=n.setTimeout(s,500)):(u=r.createElement("script"),u.type="text/"+(t.type||"javascript"),u.src=t.url);u.onload=u.onreadystatechange=o;u.onerror=e;u.async=!1;u.defer=!1;t.errorTimeout=n.setTimeout(function(){e({type:"timeout"})},7e3);f=r.head||r.getElementsByTagName("head")[0];f.insertBefore(u,f.lastChild)}function vt(){for(var t,u=r.getElementsByTagName("script"),n=0,f=u.length;n<f;n++)if(t=u[n].getAttribute("data-headjs-load"),!!t){i.load(t);return}}function yt(n,t){var v,p,e;return n===r?(o?f(t):d.push(t),i):(s(n)&&(t=n,n="ALL"),a(n))?(v={},u(n,function(n){v[n]=c[n];i.ready(n,function(){y(v)&&f(t)})}),i):typeof n!="string"||!s(t)?i:(p=c[n],p&&p.state===l||n==="ALL"&&y()&&o)?(f(t),i):(e=h[n],e?e.push(t):e=h[n]=[t],i)}function e(){if(!r.body){n.clearTimeout(i.readyTimeout);i.readyTimeout=n.setTimeout(e,50);return}o||(o=!0,vt(),u(d,function(n){f(n)}))}function k(){r.addEventListener?(r.removeEventListener("DOMContentLoaded",k,!1),e()):r.readyState==="complete"&&(r.detachEvent("onreadystatechange",k),e())}var r=n.document,d=[],h={},c={},ut="async"in r.createElement("script")||"MozAppearance"in r.documentElement.style||n.opera,o,g=n.head_conf&&n.head_conf.head||"head",i=n[g]=n[g]||function(){i.ready.apply(null,arguments)},nt=1,ft=2,tt=3,l=4,p;if(r.readyState==="complete")e();else if(r.addEventListener)r.addEventListener("DOMContentLoaded",k,!1),n.addEventListener("load",e,!1);else{r.attachEvent("onreadystatechange",k);n.attachEvent("onload",e);p=!1;try{p=!n.frameElement&&r.documentElement}catch(wt){}p&&p.doScroll&&function pt(){if(!o){try{p.doScroll("left")}catch(t){n.clearTimeout(i.readyTimeout);i.readyTimeout=n.setTimeout(pt,50);return}e()}}()}i.load=i.js=ut?lt:ct;i.test=ot;i.ready=yt;i.ready(r,function(){y()&&u(h.ALL,function(n){f(n)});i.feature&&i.feature("domloaded",!0)})})(window); | |
100 | /* | |
eb1537e3 | 101 | //# sourceMappingURL=head.load.min.js.map |
efce8a94 | 102 | */ |
eb1537e3 | 103 | |
0937237b AE |
104 | /** |
105 | * Provides a hashCode() method for strings, similar to Java's String.hashCode(). | |
106 | * | |
107 | * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ | |
108 | */ | |
109 | String.prototype.hashCode = function() { | |
110 | var $char; | |
111 | var $hash = 0; | |
112 | ||
113 | if (this.length) { | |
114 | for (var $i = 0, $length = this.length; $i < $length; $i++) { | |
115 | $char = this.charCodeAt($i); | |
116 | $hash = (($hash << 5) - $hash) + $char; | |
117 | $hash = $hash & $hash; // convert to 32bit integer | |
118 | } | |
119 | } | |
120 | ||
121 | return $hash; | |
7382b20f | 122 | }; |
0937237b | 123 | |
d5717b23 AE |
124 | /** |
125 | * Adds a Fisher-Yates shuffle algorithm for arrays. | |
126 | * | |
127 | * @see http://stackoverflow.com/a/2450976 | |
128 | */ | |
129 | function shuffle(array) { | |
130 | var currentIndex = array.length, temporaryValue, randomIndex; | |
131 | ||
132 | // While there remain elements to shuffle... | |
133 | while (0 !== currentIndex) { | |
134 | // Pick a remaining element... | |
135 | randomIndex = Math.floor(Math.random() * currentIndex); | |
136 | currentIndex -= 1; | |
137 | ||
138 | // And swap it with the current element. | |
139 | temporaryValue = array[currentIndex]; | |
140 | array[currentIndex] = array[randomIndex]; | |
141 | array[randomIndex] = temporaryValue; | |
142 | } | |
143 | ||
144 | return this; | |
145 | }; | |
146 | ||
b108eaa5 | 147 | /** |
eb69a2e9 AE |
148 | * User-Agent based browser detection and touch detection. |
149 | */ | |
49d0d797 | 150 | (function() { |
eb69a2e9 AE |
151 | var ua = navigator.userAgent.toLowerCase(); |
152 | var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) || | |
153 | /(webkit)[ \/]([\w.]+)/.exec( ua ) || | |
154 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) || | |
155 | /(msie) ([\w.]+)/.exec( ua ) || | |
156 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || | |
157 | []; | |
158 | ||
159 | var matched = { | |
160 | browser: match[ 1 ] || "", | |
161 | version: match[ 2 ] || "0" | |
162 | }; | |
163 | browser = {}; | |
164 | ||
165 | if ( matched.browser ) { | |
166 | browser[ matched.browser ] = true; | |
167 | browser.version = matched.version; | |
168 | } | |
169 | ||
170 | // Chrome is Webkit, but Webkit is also Safari. | |
171 | if ( browser.chrome ) { | |
172 | browser.webkit = true; | |
173 | } else if ( browser.webkit ) { | |
174 | browser.safari = true; | |
175 | } | |
176 | ||
177 | jQuery.browser = browser; | |
555561be | 178 | jQuery.browser.touch = (!!('ontouchstart' in window) || (!!('msMaxTouchPoints' in window.navigator) && window.navigator.msMaxTouchPoints > 0)); |
9899046c AE |
179 | |
180 | // detect smartphones | |
f7461bbb | 181 | jQuery.browser.smartphone = ($('html').css('caption-side') == 'bottom'); |
e3f96cf0 | 182 | |
60c1c067 AE |
183 | // allow plugins to detect the used editor, value should be the same as the $.browser.<editorName> key |
184 | jQuery.browser.editor = 'redactor'; | |
185 | ||
186 | // CKEditor support (removed in WCF 2.1), do NOT remove this variable for the sake for compatibility | |
187 | jQuery.browser.ckeditor = false; | |
188 | ||
189 | // Redactor support | |
190 | jQuery.browser.redactor = true; | |
f52b6fc6 AE |
191 | |
192 | // properly detect IE11 | |
193 | if (jQuery.browser.mozilla && ua.match(/trident/)) { | |
194 | jQuery.browser.mozilla = false; | |
195 | jQuery.browser.msie = true; | |
196 | } | |
49d0d797 MW |
197 | })(); |
198 | ||
3f42eecb | 199 | /** |
43392e40 | 200 | * jQuery.browser.mobile (http://detectmobilebrowser.com/) |
60c25707 | 201 | * |
43392e40 | 202 | * jQuery.browser.mobile will be true if the browser is a mobile device |
60c25707 | 203 | * |
43392e40 | 204 | **/ |
74810514 | 205 | (function(a){(jQuery.browser=jQuery.browser||{}).mobile=/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))})(navigator.userAgent||navigator.vendor||window.opera); |
3f42eecb | 206 | |
158bd3ca TD |
207 | /** |
208 | * Initialize WCF namespace | |
209 | */ | |
210 | var WCF = {}; | |
211 | ||
212 | /** | |
213 | * Extends jQuery with additional methods. | |
214 | */ | |
215 | $.extend(true, { | |
a009a689 | 216 | /** |
acf25f0f | 217 | * Removes the given value from the given array and returns the array. |
a009a689 MS |
218 | * |
219 | * @param array array | |
220 | * @param mixed element | |
221 | * @return array | |
222 | */ | |
223 | removeArrayValue: function(array, value) { | |
224 | return $.grep(array, function(element, index) { | |
225 | return value !== element; | |
226 | }); | |
227 | }, | |
228 | ||
158bd3ca TD |
229 | /** |
230 | * Escapes an ID to work with jQuery selectors. | |
7b608580 | 231 | * |
158bd3ca TD |
232 | * @see http://docs.jquery.com/Frequently_Asked_Questions#How_do_I_select_an_element_by_an_ID_that_has_characters_used_in_CSS_notation.3F |
233 | * @param string id | |
234 | * @return string | |
235 | */ | |
236 | wcfEscapeID: function(id) { | |
237 | return id.replace(/(:|\.)/g, '\\$1'); | |
238 | }, | |
239 | ||
240 | /** | |
241 | * Returns true if given ID exists within DOM. | |
242 | * | |
243 | * @param string id | |
244 | * @return boolean | |
245 | */ | |
246 | wcfIsset: function(id) { | |
247 | return !!$('#' + $.wcfEscapeID(id)).length; | |
2b4d2743 | 248 | }, |
9f959ced | 249 | |
2b4d2743 AE |
250 | /** |
251 | * Returns the length of an object. | |
252 | * | |
253 | * @param object targetObject | |
254 | * @return integer | |
255 | */ | |
256 | getLength: function(targetObject) { | |
257 | var $length = 0; | |
9f959ced | 258 | |
2b4d2743 AE |
259 | for (var $key in targetObject) { |
260 | if (targetObject.hasOwnProperty($key)) { | |
261 | $length++; | |
262 | } | |
263 | } | |
7b608580 | 264 | |
2b4d2743 | 265 | return $length; |
158bd3ca TD |
266 | } |
267 | }); | |
268 | ||
269 | /** | |
270 | * Extends jQuery's chainable methods. | |
271 | */ | |
272 | $.fn.extend({ | |
273 | /** | |
404c9abe | 274 | * Returns tag name of first jQuery element. |
158bd3ca TD |
275 | * |
276 | * @returns string | |
277 | */ | |
278 | getTagName: function() { | |
d487dd36 | 279 | return (this.length) ? this.get(0).tagName.toLowerCase() : ''; |
158bd3ca TD |
280 | }, |
281 | ||
282 | /** | |
283 | * Returns the dimensions for current element. | |
284 | * | |
285 | * @see http://api.jquery.com/hidden-selector/ | |
286 | * @param string type | |
287 | * @return object | |
288 | */ | |
289 | getDimensions: function(type) { | |
290 | var dimensions = css = {}; | |
291 | var wasHidden = false; | |
292 | ||
293 | // show element to retrieve dimensions and restore them later | |
294 | if (this.is(':hidden')) { | |
69057ad5 | 295 | css = WCF.getInlineCSS(this); |
158bd3ca TD |
296 | |
297 | wasHidden = true; | |
298 | ||
299 | this.css({ | |
300 | display: 'block', | |
301 | visibility: 'hidden' | |
302 | }); | |
303 | } | |
304 | ||
305 | switch (type) { | |
306 | case 'inner': | |
307 | dimensions = { | |
308 | height: this.innerHeight(), | |
309 | width: this.innerWidth() | |
310 | }; | |
311 | break; | |
312 | ||
313 | case 'outer': | |
314 | dimensions = { | |
315 | height: this.outerHeight(), | |
316 | width: this.outerWidth() | |
317 | }; | |
318 | break; | |
319 | ||
320 | default: | |
321 | dimensions = { | |
322 | height: this.height(), | |
323 | width: this.width() | |
324 | }; | |
325 | break; | |
326 | } | |
327 | ||
328 | // restore previous settings | |
329 | if (wasHidden) { | |
69057ad5 | 330 | WCF.revertInlineCSS(this, css, [ 'display', 'visibility' ]); |
158bd3ca TD |
331 | } |
332 | ||
333 | return dimensions; | |
334 | }, | |
335 | ||
336 | /** | |
337 | * Returns the offsets for current element, defaults to position | |
338 | * relative to document. | |
339 | * | |
340 | * @see http://api.jquery.com/hidden-selector/ | |
341 | * @param string type | |
342 | * @return object | |
343 | */ | |
344 | getOffsets: function(type) { | |
345 | var offsets = css = {}; | |
346 | var wasHidden = false; | |
347 | ||
348 | // show element to retrieve dimensions and restore them later | |
349 | if (this.is(':hidden')) { | |
69057ad5 | 350 | css = WCF.getInlineCSS(this); |
158bd3ca TD |
351 | wasHidden = true; |
352 | ||
353 | this.css({ | |
354 | display: 'block', | |
355 | visibility: 'hidden' | |
356 | }); | |
357 | } | |
358 | ||
359 | switch (type) { | |
360 | case 'offset': | |
361 | offsets = this.offset(); | |
362 | break; | |
363 | ||
364 | case 'position': | |
365 | default: | |
366 | offsets = this.position(); | |
367 | break; | |
368 | } | |
369 | ||
370 | // restore previous settings | |
371 | if (wasHidden) { | |
69057ad5 | 372 | WCF.revertInlineCSS(this, css, [ 'display', 'visibility' ]); |
158bd3ca TD |
373 | } |
374 | ||
375 | return offsets; | |
376 | }, | |
377 | ||
378 | /** | |
379 | * Changes element's position to 'absolute' or 'fixed' while maintaining it's | |
380 | * current position relative to viewport. Optionally removes element from | |
381 | * current DOM-node and moving it into body-element (useful for drag & drop) | |
382 | * | |
383 | * @param boolean rebase | |
384 | * @return object | |
385 | */ | |
386 | makePositioned: function(position, rebase) { | |
387 | if (position != 'absolute' && position != 'fixed') { | |
388 | position = 'absolute'; | |
389 | } | |
390 | ||
391 | var $currentPosition = this.getOffsets('position'); | |
392 | this.css({ | |
393 | position: position, | |
394 | left: $currentPosition.left, | |
395 | margin: 0, | |
396 | top: $currentPosition.top | |
397 | }); | |
398 | ||
399 | if (rebase) { | |
400 | this.remove().appentTo('body'); | |
401 | } | |
402 | ||
403 | return this; | |
404 | }, | |
405 | ||
406 | /** | |
407 | * Disables a form element. | |
408 | * | |
06355ec3 | 409 | * @return jQuery |
158bd3ca TD |
410 | */ |
411 | disable: function() { | |
412 | return this.attr('disabled', 'disabled'); | |
413 | }, | |
414 | ||
415 | /** | |
416 | * Enables a form element. | |
417 | * | |
418 | * @return jQuery | |
419 | */ | |
420 | enable: function() { | |
421 | return this.removeAttr('disabled'); | |
422 | }, | |
9f959ced | 423 | |
b3991cb3 AE |
424 | /** |
425 | * Returns the element's id. If none is set, a random unique | |
426 | * ID will be assigned. | |
427 | * | |
428 | * @return string | |
429 | */ | |
430 | wcfIdentify: function() { | |
431 | if (!this.attr('id')) { | |
432 | this.attr('id', WCF.getRandomID()); | |
433 | } | |
9f959ced | 434 | |
b3991cb3 AE |
435 | return this.attr('id'); |
436 | }, | |
158bd3ca | 437 | |
0bfde690 AE |
438 | /** |
439 | * Returns the caret position of current element. If the element | |
440 | * does not equal input[type=text], input[type=password] or | |
441 | * textarea, -1 is returned. | |
442 | * | |
443 | * @return integer | |
444 | */ | |
445 | getCaret: function() { | |
404c9abe | 446 | if (this.is('input')) { |
0bfde690 AE |
447 | if (this.attr('type') != 'text' && this.attr('type') != 'password') { |
448 | return -1; | |
449 | } | |
450 | } | |
404c9abe | 451 | else if (!this.is('textarea')) { |
0bfde690 AE |
452 | return -1; |
453 | } | |
454 | ||
455 | var $position = 0; | |
456 | var $element = this.get(0); | |
457 | if (document.selection) { // IE 8 | |
458 | // set focus to enable caret on this element | |
459 | this.focus(); | |
460 | ||
461 | var $selection = document.selection.createRange(); | |
462 | $selection.moveStart('character', -this.val().length); | |
463 | $position = $selection.text.length; | |
464 | } | |
465 | else if ($element.selectionStart || $element.selectionStart == '0') { // Opera, Chrome, Firefox, Safari, IE 9+ | |
466 | $position = parseInt($element.selectionStart); | |
467 | } | |
468 | ||
469 | return $position; | |
470 | }, | |
471 | ||
f73ff47d TD |
472 | /** |
473 | * Sets the caret position of current element. If the element | |
474 | * does not equal input[type=text], input[type=password] or | |
475 | * textarea, false is returned. | |
476 | * | |
477 | * @param integer position | |
478 | * @return boolean | |
479 | */ | |
480 | setCaret: function (position) { | |
404c9abe | 481 | if (this.is('input')) { |
f73ff47d TD |
482 | if (this.attr('type') != 'text' && this.attr('type') != 'password') { |
483 | return false; | |
484 | } | |
485 | } | |
404c9abe | 486 | else if (!this.is('textarea')) { |
f73ff47d TD |
487 | return false; |
488 | } | |
489 | ||
490 | var $element = this.get(0); | |
491 | ||
492 | // set focus to enable caret on this element | |
493 | this.focus(); | |
494 | if (document.selection) { // IE 8 | |
495 | var $selection = document.selection.createRange(); | |
496 | $selection.moveStart('character', position); | |
497 | $selection.moveEnd('character', 0); | |
498 | $selection.select(); | |
499 | } | |
500 | else if ($element.selectionStart || $element.selectionStart == '0') { // Opera, Chrome, Firefox, Safari, IE 9+ | |
501 | $element.selectionStart = position; | |
502 | $element.selectionEnd = position; | |
503 | } | |
504 | ||
505 | return true; | |
506 | }, | |
507 | ||
158bd3ca TD |
508 | /** |
509 | * Shows an element by sliding and fading it into viewport. | |
510 | * | |
511 | * @param string direction | |
512 | * @param object callback | |
453aced6 | 513 | * @param integer duration |
158bd3ca TD |
514 | * @returns jQuery |
515 | */ | |
453aced6 | 516 | wcfDropIn: function(direction, callback, duration) { |
158bd3ca | 517 | if (!direction) direction = 'up'; |
453aced6 | 518 | if (!duration || !parseInt(duration)) duration = 200; |
158bd3ca | 519 | |
404c9abe | 520 | return this.show(WCF.getEffect(this, 'drop'), { direction: direction }, duration, callback); |
158bd3ca TD |
521 | }, |
522 | ||
523 | /** | |
524 | * Hides an element by sliding and fading it out the viewport. | |
525 | * | |
526 | * @param string direction | |
527 | * @param object callback | |
453aced6 | 528 | * @param integer duration |
158bd3ca TD |
529 | * @returns jQuery |
530 | */ | |
453aced6 | 531 | wcfDropOut: function(direction, callback, duration) { |
158bd3ca | 532 | if (!direction) direction = 'down'; |
453aced6 | 533 | if (!duration || !parseInt(duration)) duration = 200; |
158bd3ca | 534 | |
404c9abe | 535 | return this.hide(WCF.getEffect(this, 'drop'), { direction: direction }, duration, callback); |
158bd3ca TD |
536 | }, |
537 | ||
538 | /** | |
539 | * Shows an element by blinding it up. | |
540 | * | |
541 | * @param string direction | |
542 | * @param object callback | |
78e4d558 | 543 | * @param integer duration |
158bd3ca TD |
544 | * @returns jQuery |
545 | */ | |
78e4d558 | 546 | wcfBlindIn: function(direction, callback, duration) { |
158bd3ca | 547 | if (!direction) direction = 'vertical'; |
78e4d558 | 548 | if (!duration || !parseInt(duration)) duration = 200; |
158bd3ca | 549 | |
404c9abe | 550 | return this.show(WCF.getEffect(this, 'blind'), { direction: direction }, duration, callback); |
158bd3ca TD |
551 | }, |
552 | ||
553 | /** | |
554 | * Hides an element by blinding it down. | |
555 | * | |
556 | * @param string direction | |
557 | * @param object callback | |
78e4d558 | 558 | * @param integer duration |
158bd3ca TD |
559 | * @returns jQuery |
560 | */ | |
78e4d558 | 561 | wcfBlindOut: function(direction, callback, duration) { |
158bd3ca | 562 | if (!direction) direction = 'vertical'; |
78e4d558 | 563 | if (!duration || !parseInt(duration)) duration = 200; |
158bd3ca | 564 | |
404c9abe | 565 | return this.hide(WCF.getEffect(this, 'blind'), { direction: direction }, duration, callback); |
158bd3ca TD |
566 | }, |
567 | ||
568 | /** | |
569 | * Highlights an element. | |
570 | * | |
571 | * @param object options | |
572 | * @param object callback | |
573 | * @returns jQuery | |
574 | */ | |
575 | wcfHighlight: function(options, callback) { | |
576 | return this.effect('highlight', options, 600, callback); | |
25763f41 | 577 | }, |
9f959ced | 578 | |
25763f41 AE |
579 | /** |
580 | * Shows an element by fading it in. | |
581 | * | |
582 | * @param object callback | |
583 | * @param integer duration | |
584 | * @returns jQuery | |
585 | */ | |
586 | wcfFadeIn: function(callback, duration) { | |
587 | if (!duration || !parseInt(duration)) duration = 200; | |
588 | ||
404c9abe | 589 | return this.show(WCF.getEffect(this, 'fade'), { }, duration, callback); |
25763f41 | 590 | }, |
9f959ced | 591 | |
25763f41 AE |
592 | /** |
593 | * Hides an element by fading it out. | |
594 | * | |
595 | * @param object callback | |
596 | * @param integer duration | |
597 | * @returns jQuery | |
598 | */ | |
599 | wcfFadeOut: function(callback, duration) { | |
600 | if (!duration || !parseInt(duration)) duration = 200; | |
601 | ||
404c9abe | 602 | return this.hide(WCF.getEffect(this, 'fade'), { }, duration, callback); |
158bd3ca TD |
603 | } |
604 | }); | |
605 | ||
606 | /** | |
607 | * WoltLab Community Framework core methods | |
608 | */ | |
609 | $.extend(WCF, { | |
5a1b4042 AE |
610 | /** |
611 | * count of active dialogs | |
612 | * @var integer | |
613 | */ | |
614 | activeDialogs: 0, | |
615 | ||
158bd3ca | 616 | /** |
4da866ad | 617 | * Counter for dynamic element ids |
7b608580 | 618 | * |
158bd3ca TD |
619 | * @var integer |
620 | */ | |
621 | _idCounter: 0, | |
9f959ced | 622 | |
158bd3ca TD |
623 | /** |
624 | * Returns a dynamically created id. | |
625 | * | |
a9e6c5a7 | 626 | * @see https://github.com/sstephenson/prototype/blob/5e5cfff7c2c253eaf415c279f9083b4650cd4506/src/prototype/dom/dom.js#L1789 |
158bd3ca TD |
627 | * @return string |
628 | */ | |
629 | getRandomID: function() { | |
630 | var $elementID = ''; | |
631 | ||
632 | do { | |
633 | $elementID = 'wcf' + this._idCounter++; | |
634 | } | |
635 | while ($.wcfIsset($elementID)); | |
636 | ||
637 | return $elementID; | |
638 | }, | |
639 | ||
640 | /** | |
641 | * Wrapper for $.inArray which returns boolean value instead of | |
642 | * index value, similar to PHP's in_array(). | |
643 | * | |
644 | * @param mixed needle | |
645 | * @param array haystack | |
646 | * @return boolean | |
647 | */ | |
648 | inArray: function(needle, haystack) { | |
649 | return ($.inArray(needle, haystack) != -1); | |
650 | }, | |
651 | ||
652 | /** | |
653 | * Adjusts effect for partially supported elements. | |
654 | * | |
404c9abe | 655 | * @param jQuery object |
158bd3ca TD |
656 | * @param string effect |
657 | * @return string | |
658 | */ | |
404c9abe | 659 | getEffect: function(object, effect) { |
158bd3ca | 660 | // most effects are not properly supported on table rows, use highlight instead |
404c9abe | 661 | if (object.is('tr')) { |
158bd3ca TD |
662 | return 'highlight'; |
663 | } | |
664 | ||
665 | return effect; | |
69057ad5 AE |
666 | }, |
667 | ||
668 | /** | |
669 | * Returns inline CSS for given element. | |
670 | * | |
671 | * @param jQuery element | |
672 | * @return object | |
673 | */ | |
674 | getInlineCSS: function(element) { | |
675 | var $inlineStyles = { }; | |
676 | var $style = element.attr('style'); | |
677 | ||
678 | // no style tag given or empty | |
679 | if (!$style) { | |
680 | return { }; | |
681 | } | |
682 | ||
683 | $style = $style.split(';'); | |
684 | for (var $i = 0, $length = $style.length; $i < $length; $i++) { | |
685 | var $fragment = $.trim($style[$i]); | |
686 | if ($fragment == '') { | |
687 | continue; | |
688 | } | |
689 | ||
690 | $fragment = $fragment.split(':'); | |
691 | $inlineStyles[$.trim($fragment[0])] = $.trim($fragment[1]); | |
692 | } | |
693 | ||
694 | return $inlineStyles; | |
695 | }, | |
696 | ||
697 | /** | |
698 | * Reverts inline CSS or negates a previously set property. | |
699 | * | |
700 | * @param jQuery element | |
701 | * @param object inlineCSS | |
702 | * @param array<string> targetProperties | |
703 | */ | |
704 | revertInlineCSS: function(element, inlineCSS, targetProperties) { | |
705 | for (var $i = 0, $length = targetProperties.length; $i < $length; $i++) { | |
706 | var $property = targetProperties[$i]; | |
707 | ||
708 | // revert inline CSS | |
709 | if (inlineCSS[$property]) { | |
710 | element.css($property, inlineCSS[$property]); | |
711 | } | |
712 | else { | |
713 | // negate inline CSS | |
714 | element.css($property, ''); | |
715 | } | |
716 | } | |
158bd3ca TD |
717 | } |
718 | }); | |
719 | ||
b8b58a7e AE |
720 | /** |
721 | * Browser related functions. | |
722 | */ | |
723 | WCF.Browser = { | |
724 | /** | |
725 | * determines if browser is chrome | |
726 | * @var boolean | |
727 | */ | |
728 | _isChrome: null, | |
729 | ||
730 | /** | |
731 | * Returns true, if browser is Chrome, Chromium or using GoogleFrame for Internet Explorer. | |
732 | * | |
733 | * @return boolean | |
734 | */ | |
735 | isChrome: function() { | |
736 | if (this._isChrome === null) { | |
737 | this._isChrome = false; | |
738 | if (/chrom(e|ium)/.test(navigator.userAgent.toLowerCase())) { | |
739 | this._isChrome = true; | |
740 | } | |
741 | } | |
742 | ||
743 | return this._isChrome; | |
744 | } | |
745 | }; | |
746 | ||
0d6ea23f | 747 | /** |
184a8d6d AE |
748 | * Dropdown API |
749 | */ | |
750 | WCF.Dropdown = { | |
751 | /** | |
752 | * list of callbacks | |
753 | * @var object | |
754 | */ | |
755 | _callbacks: { }, | |
756 | ||
757 | /** | |
758 | * initialization state | |
759 | * @var boolean | |
760 | */ | |
761 | _didInit: false, | |
762 | ||
763 | /** | |
764 | * list of registered dropdowns | |
765 | * @var object | |
766 | */ | |
767 | _dropdowns: { }, | |
768 | ||
3ef8dee9 AE |
769 | /** |
770 | * container for dropdown menus | |
771 | * @var object | |
772 | */ | |
773 | _menuContainer: null, | |
774 | ||
775 | /** | |
776 | * list of registered dropdown menus | |
777 | * @var object | |
778 | */ | |
779 | _menus: { }, | |
780 | ||
184a8d6d AE |
781 | /** |
782 | * Initializes dropdowns. | |
783 | */ | |
784 | init: function() { | |
3ef8dee9 AE |
785 | if (this._menuContainer === null) { |
786 | this._menuContainer = $('<div id="dropdownMenuContainer" />').appendTo(document.body); | |
787 | } | |
788 | ||
184a8d6d | 789 | var self = this; |
3ef8dee9 AE |
790 | $('.dropdownToggle:not(.jsDropdownEnabled)').each(function(index, button) { |
791 | self.initDropdown($(button), false); | |
184a8d6d AE |
792 | }); |
793 | ||
794 | if (!this._didInit) { | |
795 | this._didInit = true; | |
796 | ||
797 | WCF.CloseOverlayHandler.addCallback('WCF.Dropdown', $.proxy(this._closeAll, this)); | |
798 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Dropdown', $.proxy(this.init, this)); | |
978fdd0a | 799 | $(document).on('scroll', $.proxy(this._scroll, this)); |
184a8d6d | 800 | } |
289b4a48 MS |
801 | }, |
802 | ||
803 | /** | |
804 | * Handles dropdown positions in overlays when scrolling in the overlay. | |
805 | * | |
806 | * @param object event | |
807 | */ | |
808 | _dialogScroll: function(event) { | |
809 | var $dialogContent = $(event.currentTarget); | |
810 | $dialogContent.find('.dropdown.dropdownOpen').each(function(index, element) { | |
811 | var $dropdown = $(element); | |
d3fb0283 MS |
812 | var $dropdownID = $dropdown.wcfIdentify(); |
813 | var $dropdownOffset = $dropdown.offset(); | |
814 | var $dialogContentOffset = $dialogContent.offset(); | |
815 | ||
816 | var $verticalScrollTolerance = $(element).height() / 2; | |
289b4a48 | 817 | |
7b608580 | 818 | // check if dropdown toggle is still (partially) visible |
d3fb0283 MS |
819 | if ($dropdownOffset.top + $verticalScrollTolerance <= $dialogContentOffset.top) { |
820 | // top check | |
821 | WCF.Dropdown.toggleDropdown($dropdownID); | |
822 | } | |
823 | else if ($dropdownOffset.top >= $dialogContentOffset.top + $dialogContent.height()) { | |
824 | // bottom check | |
825 | WCF.Dropdown.toggleDropdown($dropdownID); | |
826 | } | |
827 | else if ($dropdownOffset.left <= $dialogContentOffset.left) { | |
828 | // left check | |
829 | WCF.Dropdown.toggleDropdown($dropdownID); | |
289b4a48 | 830 | } |
d3fb0283 MS |
831 | else if ($dropdownOffset.left >= $dialogContentOffset.left + $dialogContent.width()) { |
832 | // right check | |
833 | WCF.Dropdown.toggleDropdown($dropdownID); | |
289b4a48 MS |
834 | } |
835 | else { | |
836 | WCF.Dropdown.setAlignmentByID($dropdown.wcfIdentify()); | |
837 | } | |
838 | }); | |
839 | }, | |
840 | ||
841 | /** | |
842 | * Handles dropdown positions in overlays when scrolling in the document. | |
843 | * | |
844 | * @param object event | |
845 | */ | |
846 | _scroll: function(event) { | |
847 | for (var $containerID in this._dropdowns) { | |
848 | var $dropdown = this._dropdowns[$containerID]; | |
849 | if ($dropdown.data('isOverlayDropdownButton') && $dropdown.hasClass('dropdownOpen')) { | |
850 | this.setAlignmentByID($containerID); | |
851 | } | |
852 | } | |
184a8d6d AE |
853 | }, |
854 | ||
3ef8dee9 AE |
855 | /** |
856 | * Initializes a dropdown. | |
857 | * | |
858 | * @param jQuery button | |
859 | * @param boolean isLazyInitialization | |
860 | */ | |
861 | initDropdown: function(button, isLazyInitialization) { | |
862 | if (button.hasClass('jsDropdownEnabled') || button.data('target')) { | |
863 | return; | |
864 | } | |
865 | ||
866 | var $dropdown = button.parents('.dropdown'); | |
867 | if (!$dropdown.length) { | |
868 | // broken dropdown, ignore | |
7b608580 | 869 | console.debug("[WCF.Dropdown] Invalid dropdown passed, button '" + button.wcfIdentify() + "' does not have a parent with .dropdown, aborting."); |
3ef8dee9 AE |
870 | return; |
871 | } | |
872 | ||
873 | var $dropdownMenu = button.next('.dropdownMenu'); | |
874 | if (!$dropdownMenu.length) { | |
875 | // broken dropdown, ignore | |
7b608580 | 876 | console.debug("[WCF.Dropdown] Invalid dropdown passed, dropdown '" + $dropdown.wcfIdentify() + "' does not have a dropdown menu, aborting."); |
3ef8dee9 AE |
877 | return; |
878 | } | |
879 | ||
880 | $dropdownMenu.detach().appendTo(this._menuContainer); | |
881 | var $containerID = $dropdown.wcfIdentify(); | |
882 | if (!this._dropdowns[$containerID]) { | |
883 | button.addClass('jsDropdownEnabled').click($.proxy(this._toggle, this)); | |
884 | ||
885 | this._dropdowns[$containerID] = $dropdown; | |
886 | this._menus[$containerID] = $dropdownMenu; | |
887 | } | |
888 | ||
889 | button.data('target', $containerID); | |
890 | ||
891 | if (isLazyInitialization) { | |
892 | button.trigger('click'); | |
893 | } | |
894 | }, | |
895 | ||
a203335d MS |
896 | /** |
897 | * Removes the dropdown with the given container id. | |
898 | * | |
899 | * @param string containerID | |
900 | */ | |
901 | removeDropdown: function(containerID) { | |
902 | if (this._menus[containerID]) { | |
903 | $(this._menus[containerID]).remove(); | |
904 | delete this._menus[containerID]; | |
905 | delete this._dropdowns[containerID]; | |
906 | } | |
907 | }, | |
908 | ||
38d131ce AE |
909 | /** |
910 | * Initializes a dropdown fragment which behaves like a usual dropdown | |
911 | * but is not controlled by a trigger element. | |
912 | * | |
913 | * @param jQuery dropdown | |
914 | * @param jQuery dropdownMenu | |
915 | */ | |
916 | initDropdownFragment: function(dropdown, dropdownMenu) { | |
917 | var $containerID = dropdown.wcfIdentify(); | |
918 | if (this._dropdowns[$containerID]) { | |
919 | console.debug("[WCF.Dropdown] Cannot register dropdown identified by '" + $containerID + "' as a fragement."); | |
920 | return; | |
921 | } | |
922 | ||
923 | this._dropdowns[$containerID] = dropdown; | |
924 | this._menus[$containerID] = dropdownMenu.detach().appendTo(this._menuContainer); | |
925 | }, | |
926 | ||
184a8d6d AE |
927 | /** |
928 | * Registers a callback notified upon dropdown state change. | |
929 | * | |
930 | * @param string identifier | |
931 | * @var object callback | |
932 | */ | |
933 | registerCallback: function(identifier, callback) { | |
934 | if (!$.isFunction(callback)) { | |
935 | console.debug("[WCF.Dropdown] Callback for '" + identifier + "' is invalid"); | |
936 | return false; | |
937 | } | |
938 | ||
939 | if (!this._callbacks[identifier]) { | |
940 | this._callbacks[identifier] = [ ]; | |
941 | } | |
942 | ||
943 | this._callbacks[identifier].push(callback); | |
944 | }, | |
945 | ||
946 | /** | |
947 | * Toggles a dropdown. | |
948 | * | |
949 | * @param object event | |
3ef8dee9 | 950 | * @param string targetID |
184a8d6d | 951 | */ |
3ef8dee9 AE |
952 | _toggle: function(event, targetID) { |
953 | var $targetID = (event === null) ? targetID : $(event.currentTarget).data('target'); | |
2b8f0792 | 954 | |
289b4a48 MS |
955 | // check if 'isOverlayDropdownButton' is set which indicates if |
956 | // the dropdown toggle is in an overlay | |
957 | var $target = this._dropdowns[$targetID]; | |
958 | if ($target && $target.data('isOverlayDropdownButton') === undefined) { | |
959 | var $dialogContent = $target.parents('.dialogContent'); | |
960 | $target.data('isOverlayDropdownButton', $dialogContent.length > 0); | |
961 | ||
962 | if ($dialogContent.length) { | |
963 | $dialogContent.on('scroll', this._dialogScroll); | |
964 | } | |
965 | } | |
966 | ||
2b8f0792 AE |
967 | // close all dropdowns |
968 | for (var $containerID in this._dropdowns) { | |
969 | var $dropdown = this._dropdowns[$containerID]; | |
3ef8dee9 AE |
970 | var $dropdownMenu = this._menus[$containerID]; |
971 | ||
2b8f0792 AE |
972 | if ($dropdown.hasClass('dropdownOpen')) { |
973 | $dropdown.removeClass('dropdownOpen'); | |
3ef8dee9 AE |
974 | $dropdownMenu.removeClass('dropdownOpen'); |
975 | ||
976 | this._notifyCallbacks($containerID, 'close'); | |
2b8f0792 AE |
977 | } |
978 | else if ($containerID === $targetID) { | |
979 | $dropdown.addClass('dropdownOpen'); | |
3ef8dee9 AE |
980 | $dropdownMenu.addClass('dropdownOpen'); |
981 | ||
982 | this._notifyCallbacks($containerID, 'open'); | |
71662ae8 | 983 | |
3ef8dee9 | 984 | this.setAlignment($dropdown, $dropdownMenu); |
2b8f0792 | 985 | } |
184a8d6d AE |
986 | } |
987 | ||
38d131ce AE |
988 | if (event !== null) { |
989 | event.stopPropagation(); | |
990 | return false; | |
991 | } | |
184a8d6d AE |
992 | }, |
993 | ||
3ef8dee9 AE |
994 | /** |
995 | * Toggles a dropdown. | |
996 | * | |
997 | * @param string containerID | |
998 | */ | |
999 | toggleDropdown: function(containerID) { | |
1000 | this._toggle(null, containerID); | |
1001 | }, | |
1002 | ||
1003 | /** | |
1004 | * Returns dropdown by container id. | |
1005 | * | |
1006 | * @param string containerID | |
1007 | * @return jQuery | |
1008 | */ | |
1009 | getDropdown: function(containerID) { | |
1010 | if (this._dropdowns[containerID]) { | |
1011 | return this._dropdowns[containerID]; | |
1012 | } | |
1013 | ||
1014 | return null; | |
1015 | }, | |
1016 | ||
1017 | /** | |
1018 | * Returns dropdown menu by container id. | |
1019 | * | |
1020 | * @param string containerID | |
1021 | * @return jQuery | |
1022 | */ | |
1023 | getDropdownMenu: function(containerID) { | |
1024 | if (this._menus[containerID]) { | |
1025 | return this._menus[containerID]; | |
1026 | } | |
1027 | ||
1028 | return null; | |
1029 | }, | |
1030 | ||
1031 | /** | |
1032 | * Sets alignment for given container id. | |
1033 | * | |
1034 | * @param string containerID | |
1035 | */ | |
1036 | setAlignmentByID: function(containerID) { | |
1037 | var $dropdown = this.getDropdown(containerID); | |
1038 | if ($dropdown === null) { | |
1039 | console.debug("[WCF.Dropdown] Unable to find dropdown identified by '" + containerID + "'"); | |
1040 | } | |
1041 | ||
1042 | var $dropdownMenu = this.getDropdownMenu(containerID); | |
1043 | if ($dropdownMenu === null) { | |
1044 | console.debug("[WCF.Dropdown] Unable to find dropdown menu identified by '" + containerID + "'"); | |
1045 | } | |
1046 | ||
1047 | this.setAlignment($dropdown, $dropdownMenu); | |
1048 | }, | |
1049 | ||
71662ae8 AE |
1050 | /** |
1051 | * Sets alignment for dropdown. | |
1052 | * | |
1053 | * @param jQuery dropdown | |
1054 | * @param jQuery dropdownMenu | |
1055 | */ | |
1056 | setAlignment: function(dropdown, dropdownMenu) { | |
253a1b74 AE |
1057 | // force dropdown menu to be placed in the upper left corner, otherwise |
1058 | // it might cause the calculations to be a bit off if the page exceeds | |
1059 | // the window boundaries during getDimensions() making it visible | |
1060 | if (!dropdownMenu.data('isInitialized')) { | |
1061 | dropdownMenu.data('isInitialized', true).css({ left: 0, top: 0 }); | |
1062 | } | |
1063 | ||
3ef8dee9 AE |
1064 | // get dropdown position |
1065 | var $dropdownDimensions = dropdown.getDimensions('outer'); | |
1066 | var $dropdownOffsets = dropdown.getOffsets('offset'); | |
1067 | var $menuDimensions = dropdownMenu.getDimensions('outer'); | |
71662ae8 AE |
1068 | var $windowWidth = $(window).width(); |
1069 | ||
ca155b1c AE |
1070 | // check if button belongs to an i18n textarea |
1071 | var $button = dropdown.find('.dropdownToggle'); | |
1072 | if ($button.hasClass('dropdownCaptionTextarea')) { | |
1073 | // use button dimensions instead | |
1074 | $dropdownDimensions = $button.getDimensions('outer'); | |
1075 | } | |
1076 | ||
1635b5bd MS |
1077 | // get alignment |
1078 | var $align = 'left'; | |
1079 | if (($dropdownOffsets.left + $menuDimensions.width) > $windowWidth) { | |
1080 | $align = 'right'; | |
71662ae8 | 1081 | } |
3ef8dee9 | 1082 | |
1635b5bd MS |
1083 | // calculate offsets |
1084 | var $left = 'auto'; | |
1085 | var $right = 'auto'; | |
1086 | if ($align === 'left') { | |
9db77c5d | 1087 | dropdownMenu.removeClass('dropdownArrowRight'); |
3ef8dee9 | 1088 | |
ec4825f6 | 1089 | $left = $dropdownOffsets.left; |
1635b5bd MS |
1090 | } |
1091 | else { | |
1092 | dropdownMenu.addClass('dropdownArrowRight'); | |
3ef8dee9 | 1093 | |
ec4825f6 | 1094 | $right = ($windowWidth - ($dropdownOffsets.left + $dropdownDimensions.width)); |
71662ae8 | 1095 | } |
1635b5bd | 1096 | |
ec4825f6 AE |
1097 | // rtl works the same with the exception that we need to offset it with the right boundary |
1098 | if (WCF.Language.get('wcf.global.pageDirection') == 'rtl') { | |
1099 | var $oldLeft = $left; | |
1100 | var $oldRight = $right; | |
1101 | ||
1102 | // use reverse positioning | |
1103 | if ($left == 'auto') { | |
1104 | dropdownMenu.removeClass('dropdownArrowRight'); | |
1105 | } | |
1106 | else { | |
1107 | $right = $windowWidth - ($dropdownOffsets.left + $dropdownDimensions.width); | |
1108 | $left = 'auto'; | |
1109 | ||
1110 | if ($right + $menuDimensions.width > $windowWidth) { | |
1111 | // exceeded window width, restore ltr values | |
1112 | $left = $oldLeft; | |
1113 | $right = $oldRight; | |
1114 | ||
1115 | dropdownMenu.addClass('dropdownArrowRight'); | |
1116 | } | |
1117 | } | |
1118 | } | |
1119 | ||
1120 | if ($left == 'auto') $right += 'px'; | |
1121 | else $left += 'px'; | |
1122 | ||
9db77c5d AE |
1123 | // calculate vertical offset |
1124 | var $wasHidden = true; | |
1125 | if (dropdownMenu.hasClass('dropdownOpen')) { | |
1126 | $wasHidden = false; | |
1127 | dropdownMenu.removeClass('dropdownOpen'); | |
1128 | } | |
1129 | ||
1130 | var $bottom = 'auto'; | |
1131 | var $top = $dropdownOffsets.top + $dropdownDimensions.height + 7; | |
f3c5fca9 | 1132 | if ($top + $menuDimensions.height > $(window).height() + $(document).scrollTop()) { |
9db77c5d AE |
1133 | $bottom = $(window).height() - $dropdownOffsets.top + 10; |
1134 | $top = 'auto'; | |
1135 | ||
1136 | dropdownMenu.addClass('dropdownArrowBottom'); | |
1137 | } | |
1138 | else { | |
1139 | dropdownMenu.removeClass('dropdownArrowBottom'); | |
1140 | } | |
1141 | ||
1142 | if (!$wasHidden) { | |
1143 | dropdownMenu.addClass('dropdownOpen'); | |
1144 | } | |
1145 | ||
1635b5bd | 1146 | dropdownMenu.css({ |
9db77c5d | 1147 | bottom: $bottom, |
1635b5bd MS |
1148 | left: $left, |
1149 | right: $right, | |
9db77c5d | 1150 | top: $top |
1635b5bd | 1151 | }); |
71662ae8 AE |
1152 | }, |
1153 | ||
184a8d6d AE |
1154 | /** |
1155 | * Closes all dropdowns. | |
1156 | */ | |
1157 | _closeAll: function() { | |
1158 | for (var $containerID in this._dropdowns) { | |
1159 | var $dropdown = this._dropdowns[$containerID]; | |
1160 | if ($dropdown.hasClass('dropdownOpen')) { | |
1161 | $dropdown.removeClass('dropdownOpen'); | |
3ef8dee9 | 1162 | this._menus[$containerID].removeClass('dropdownOpen'); |
184a8d6d | 1163 | |
3ef8dee9 | 1164 | this._notifyCallbacks($containerID, 'close'); |
184a8d6d AE |
1165 | } |
1166 | } | |
1167 | }, | |
1168 | ||
1169 | /** | |
1170 | * Closes a dropdown without notifying callbacks. | |
1171 | * | |
1172 | * @param string containerID | |
1173 | */ | |
1174 | close: function(containerID) { | |
1175 | if (!this._dropdowns[containerID]) { | |
1176 | return; | |
1177 | } | |
1178 | ||
3ef8dee9 AE |
1179 | this._dropdowns[containerID].removeClass('dropdownMenu'); |
1180 | this._menus[containerID].removeClass('dropdownMenu'); | |
184a8d6d AE |
1181 | }, |
1182 | ||
1183 | /** | |
1184 | * Notifies callbacks. | |
1185 | * | |
3ef8dee9 | 1186 | * @param string containerID |
184a8d6d AE |
1187 | * @param string action |
1188 | */ | |
3ef8dee9 AE |
1189 | _notifyCallbacks: function(containerID, action) { |
1190 | if (!this._callbacks[containerID]) { | |
184a8d6d AE |
1191 | return; |
1192 | } | |
1193 | ||
3ef8dee9 AE |
1194 | for (var $i = 0, $length = this._callbacks[containerID].length; $i < $length; $i++) { |
1195 | this._callbacks[containerID][$i](containerID, action); | |
184a8d6d AE |
1196 | } |
1197 | } | |
1198 | }; | |
5be96f42 | 1199 | |
184a8d6d | 1200 | /** |
0d6ea23f AE |
1201 | * Clipboard API |
1202 | */ | |
1203 | WCF.Clipboard = { | |
1204 | /** | |
1205 | * action proxy object | |
1206 | * @var WCF.Action.Proxy | |
1207 | */ | |
1208 | _actionProxy: null, | |
1209 | ||
da27d58a MS |
1210 | /** |
1211 | * action objects | |
1212 | * @var object | |
1213 | */ | |
1214 | _actionObjects: {}, | |
1215 | ||
d322363b AE |
1216 | /** |
1217 | * list of clipboard containers | |
1218 | * @var jQuery | |
1219 | */ | |
67bf4eb3 | 1220 | _containers: null, |
d322363b | 1221 | |
d893cd14 AE |
1222 | /** |
1223 | * container meta data | |
1224 | * @var object | |
1225 | */ | |
1226 | _containerData: { }, | |
1227 | ||
d322363b AE |
1228 | /** |
1229 | * user has marked items | |
1230 | * @var boolean | |
1231 | */ | |
1232 | _hasMarkedItems: false, | |
1233 | ||
a009a689 | 1234 | /** |
5966cc7f MS |
1235 | * list of ids of marked objects grouped by object type |
1236 | * @var object | |
a009a689 | 1237 | */ |
5966cc7f | 1238 | _markedObjectIDs: { }, |
a009a689 | 1239 | |
0d6ea23f AE |
1240 | /** |
1241 | * current page | |
1242 | * @var string | |
1243 | */ | |
1244 | _page: '', | |
1245 | ||
607c1f32 AE |
1246 | /** |
1247 | * current page's object id | |
1248 | * @var integer | |
1249 | */ | |
1250 | _pageObjectID: 0, | |
1251 | ||
0d6ea23f AE |
1252 | /** |
1253 | * proxy object | |
1254 | * @var WCF.Action.Proxy | |
1255 | */ | |
1256 | _proxy: null, | |
1257 | ||
f892f868 AE |
1258 | /** |
1259 | * list of elements already tracked for clipboard actions | |
1260 | * @var object | |
1261 | */ | |
1262 | _trackedElements: { }, | |
1263 | ||
0d6ea23f AE |
1264 | /** |
1265 | * Initializes the clipboard API. | |
607c1f32 AE |
1266 | * |
1267 | * @param string page | |
1268 | * @param integer hasMarkedItems | |
1269 | * @param object actionObjects | |
1270 | * @param integer pageObjectID | |
0d6ea23f | 1271 | */ |
607c1f32 | 1272 | init: function(page, hasMarkedItems, actionObjects, pageObjectID) { |
0d6ea23f | 1273 | this._page = page; |
607c1f32 AE |
1274 | this._actionObjects = actionObjects || { }; |
1275 | this._hasMarkedItems = (hasMarkedItems > 0); | |
1276 | this._pageObjectID = parseInt(pageObjectID) || 0; | |
0d6ea23f AE |
1277 | |
1278 | this._actionProxy = new WCF.Action.Proxy({ | |
1279 | success: $.proxy(this._actionSuccess, this), | |
d71e5a29 | 1280 | url: 'index.php/ClipboardProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND |
0d6ea23f AE |
1281 | }); |
1282 | ||
1283 | this._proxy = new WCF.Action.Proxy({ | |
1284 | success: $.proxy(this._success, this), | |
d71e5a29 | 1285 | url: 'index.php/Clipboard/?t=' + SECURITY_TOKEN + SID_ARG_2ND |
0d6ea23f AE |
1286 | }); |
1287 | ||
1288 | // init containers first | |
6f475a52 | 1289 | this._containers = $('.jsClipboardContainer').each($.proxy(function(index, container) { |
0d6ea23f AE |
1290 | this._initContainer(container); |
1291 | }, this)); | |
d322363b AE |
1292 | |
1293 | // loads marked items | |
8b0cabda | 1294 | if (this._hasMarkedItems && this._containers.length) { |
d322363b AE |
1295 | this._loadMarkedItems(); |
1296 | } | |
f892f868 AE |
1297 | |
1298 | var self = this; | |
1299 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Clipboard', function() { | |
1300 | self._containers = $('.jsClipboardContainer').each($.proxy(function(index, container) { | |
1301 | self._initContainer(container); | |
1302 | }, self)); | |
1303 | }); | |
d322363b AE |
1304 | }, |
1305 | ||
1306 | /** | |
1307 | * Loads marked items on init. | |
1308 | */ | |
1309 | _loadMarkedItems: function() { | |
1310 | new WCF.Action.Proxy({ | |
1311 | autoSend: true, | |
1312 | data: { | |
d893cd14 | 1313 | containerData: this._containerData, |
607c1f32 AE |
1314 | pageClassName: this._page, |
1315 | pageObjectID: this._pageObjectID | |
d322363b AE |
1316 | }, |
1317 | success: $.proxy(this._loadMarkedItemsSuccess, this), | |
d71e5a29 | 1318 | url: 'index.php/ClipboardLoadMarkedItems/?t=' + SECURITY_TOKEN + SID_ARG_2ND |
d322363b AE |
1319 | }); |
1320 | }, | |
1321 | ||
92a6d759 AE |
1322 | /** |
1323 | * Reloads the list of marked items. | |
1324 | */ | |
1325 | reload: function() { | |
67bf4eb3 MS |
1326 | if (this._containers === null) { |
1327 | return; | |
1328 | } | |
1329 | ||
92a6d759 AE |
1330 | this._loadMarkedItems(); |
1331 | }, | |
1332 | ||
d322363b AE |
1333 | /** |
1334 | * Marks all returned items as marked | |
1335 | * | |
1336 | * @param object data | |
1337 | * @param string textStatus | |
1338 | * @param jQuery jqXHR | |
1339 | */ | |
1340 | _loadMarkedItemsSuccess: function(data, textStatus, jqXHR) { | |
91b5b09f AE |
1341 | this._resetMarkings(); |
1342 | ||
d322363b | 1343 | for (var $typeName in data.markedItems) { |
7ce2effc | 1344 | if (!this._markedObjectIDs[$typeName]) { |
7412629a | 1345 | this._markedObjectIDs[$typeName] = [ ]; |
7ce2effc MS |
1346 | } |
1347 | ||
d322363b | 1348 | var $objectData = data.markedItems[$typeName]; |
d322363b | 1349 | for (var $i in $objectData) { |
5966cc7f | 1350 | this._markedObjectIDs[$typeName].push($objectData[$i]); |
d322363b AE |
1351 | } |
1352 | ||
1353 | // loop through all containers | |
a009a689 | 1354 | this._containers.each($.proxy(function(index, container) { |
d322363b AE |
1355 | var $container = $(container); |
1356 | ||
1357 | // typeName does not match, continue | |
1358 | if ($container.data('type') != $typeName) { | |
1359 | return true; | |
1360 | } | |
1361 | ||
1362 | // mark items as marked | |
a009a689 | 1363 | $container.find('input.jsClipboardItem').each($.proxy(function(innerIndex, item) { |
d322363b | 1364 | var $item = $(item); |
5966cc7f | 1365 | if (WCF.inArray($item.data('objectID'), this._markedObjectIDs[$typeName])) { |
d1265cbf | 1366 | $item.prop('checked', true); |
483aaa32 AE |
1367 | |
1368 | // add marked class for element container | |
1369 | $item.parents('.jsClipboardObject').addClass('jsMarked'); | |
d322363b | 1370 | } |
a009a689 | 1371 | }, this)); |
d322363b AE |
1372 | |
1373 | // check if there is a markAll-checkbox | |
6f475a52 | 1374 | $container.find('input.jsClipboardMarkAll').each(function(innerIndex, markAll) { |
d322363b AE |
1375 | var $allItemsMarked = true; |
1376 | ||
6f475a52 | 1377 | $container.find('input.jsClipboardItem').each(function(itemIndex, item) { |
d322363b | 1378 | var $item = $(item); |
1a50aae0 | 1379 | if (!$item.prop('checked')) { |
d322363b AE |
1380 | $allItemsMarked = false; |
1381 | } | |
1382 | }); | |
1383 | ||
1384 | if ($allItemsMarked) { | |
d1265cbf | 1385 | $(markAll).prop('checked', true); |
d322363b AE |
1386 | } |
1387 | }); | |
a009a689 | 1388 | }, this)); |
d322363b AE |
1389 | } |
1390 | ||
1391 | // call success method to build item list editors | |
1392 | this._success(data, textStatus, jqXHR); | |
0d6ea23f AE |
1393 | }, |
1394 | ||
91b5b09f AE |
1395 | /** |
1396 | * Resets all checkboxes. | |
1397 | */ | |
1398 | _resetMarkings: function() { | |
5966cc7f | 1399 | this._containers.each($.proxy(function(index, container) { |
91b5b09f AE |
1400 | var $container = $(container); |
1401 | ||
5966cc7f | 1402 | this._markedObjectIDs[$container.data('type')] = [ ]; |
d1265cbf | 1403 | $container.find('input.jsClipboardItem, input.jsClipboardMarkAll').prop('checked', false); |
483aaa32 | 1404 | $container.find('.jsClipboardObject').removeClass('jsMarked'); |
5966cc7f | 1405 | }, this)); |
91b5b09f AE |
1406 | }, |
1407 | ||
0d6ea23f AE |
1408 | /** |
1409 | * Initializes a clipboard container. | |
1410 | * | |
1411 | * @param object container | |
1412 | */ | |
1413 | _initContainer: function(container) { | |
1414 | var $container = $(container); | |
d893cd14 | 1415 | var $containerID = $container.wcfIdentify(); |
0d6ea23f | 1416 | |
f892f868 AE |
1417 | if (!this._trackedElements[$containerID]) { |
1418 | $container.find('.jsClipboardMarkAll').data('hasContainer', $containerID).click($.proxy(this._markAll, this)); | |
1419 | ||
5966cc7f | 1420 | this._markedObjectIDs[$container.data('type')] = [ ]; |
f892f868 AE |
1421 | this._containerData[$container.data('type')] = {}; |
1422 | $.each($container.data(), $.proxy(function(index, element) { | |
1423 | if (index.match(/^type(.+)/)) { | |
1424 | this._containerData[$container.data('type')][WCF.String.lcfirst(index.replace(/^type/, ''))] = element; | |
1425 | } | |
1426 | }, this)); | |
1427 | ||
1428 | this._trackedElements[$containerID] = [ ]; | |
1429 | } | |
0d6ea23f | 1430 | |
f892f868 AE |
1431 | // track individual checkboxes |
1432 | $container.find('input.jsClipboardItem').each($.proxy(function(index, input) { | |
1433 | var $input = $(input); | |
1434 | var $inputID = $input.wcfIdentify(); | |
1435 | ||
1436 | if (!WCF.inArray($inputID, this._trackedElements[$containerID])) { | |
1437 | this._trackedElements[$containerID].push($inputID); | |
1438 | ||
1439 | $input.data('hasContainer', $containerID).click($.proxy(this._click, this)); | |
995afaf1 MS |
1440 | } |
1441 | }, this)); | |
0d6ea23f AE |
1442 | }, |
1443 | ||
1444 | /** | |
1445 | * Processes change checkbox state. | |
1446 | * | |
1447 | * @param object event | |
1448 | */ | |
1449 | _click: function(event) { | |
1450 | var $item = $(event.target); | |
1451 | var $objectID = $item.data('objectID'); | |
1a50aae0 | 1452 | var $isMarked = ($item.prop('checked')) ? true : false; |
0d6ea23f AE |
1453 | var $objectIDs = [ $objectID ]; |
1454 | ||
5966cc7f MS |
1455 | if ($item.data('hasContainer')) { |
1456 | var $container = $('#' + $item.data('hasContainer')); | |
1457 | var $type = $container.data('type'); | |
1458 | } | |
1459 | else { | |
1460 | var $type = $item.data('type'); | |
1461 | } | |
1462 | ||
a009a689 | 1463 | if ($isMarked) { |
5966cc7f | 1464 | this._markedObjectIDs[$type].push($objectID); |
483aaa32 | 1465 | $item.parents('.jsClipboardObject').addClass('jsMarked'); |
a009a689 MS |
1466 | } |
1467 | else { | |
5966cc7f | 1468 | this._markedObjectIDs[$type] = $.removeArrayValue(this._markedObjectIDs[$type], $objectID); |
483aaa32 | 1469 | $item.parents('.jsClipboardObject').removeClass('jsMarked'); |
a009a689 MS |
1470 | } |
1471 | ||
0d6ea23f AE |
1472 | // item is part of a container |
1473 | if ($item.data('hasContainer')) { | |
0d6ea23f AE |
1474 | // check if all items are marked |
1475 | var $markedAll = true; | |
6f475a52 | 1476 | $container.find('input.jsClipboardItem').each(function(index, containerItem) { |
0d6ea23f | 1477 | var $containerItem = $(containerItem); |
1a50aae0 | 1478 | if (!$containerItem.prop('checked')) { |
0d6ea23f AE |
1479 | $markedAll = false; |
1480 | } | |
1481 | }); | |
1482 | ||
1483 | // simulate a ticked 'markAll' checkbox | |
6f475a52 | 1484 | $container.find('.jsClipboardMarkAll').each(function(index, markAll) { |
0d6ea23f | 1485 | if ($markedAll) { |
d1265cbf | 1486 | $(markAll).prop('checked', true); |
0d6ea23f AE |
1487 | } |
1488 | else { | |
d1265cbf | 1489 | $(markAll).prop('checked', false); |
0d6ea23f AE |
1490 | } |
1491 | }); | |
1492 | } | |
0d6ea23f AE |
1493 | |
1494 | this._saveState($type, $objectIDs, $isMarked); | |
1495 | }, | |
1496 | ||
1497 | /** | |
1498 | * Marks all associated clipboard items as checked. | |
1499 | * | |
1500 | * @param object event | |
1501 | */ | |
1502 | _markAll: function(event) { | |
1503 | var $item = $(event.target); | |
1504 | var $objectIDs = [ ]; | |
1505 | var $isMarked = true; | |
1506 | ||
1507 | // if markAll object is a checkbox, allow toggling | |
404c9abe | 1508 | if ($item.is('input')) { |
1a50aae0 | 1509 | $isMarked = $item.prop('checked'); |
0d6ea23f AE |
1510 | } |
1511 | ||
0d6ea23f AE |
1512 | if ($item.data('hasContainer')) { |
1513 | var $container = $('#' + $item.data('hasContainer')); | |
1514 | var $type = $container.data('type'); | |
5966cc7f MS |
1515 | } |
1516 | else { | |
1517 | var $type = $item.data('type'); | |
1518 | } | |
1519 | ||
1520 | // handle item containers | |
1521 | if ($item.data('hasContainer')) { | |
0d6ea23f | 1522 | // toggle state for all associated items |
a009a689 | 1523 | $container.find('input.jsClipboardItem').each($.proxy(function(index, containerItem) { |
0d6ea23f | 1524 | var $containerItem = $(containerItem); |
a009a689 | 1525 | var $objectID = $containerItem.data('objectID'); |
0d6ea23f | 1526 | if ($isMarked) { |
1a50aae0 | 1527 | if (!$containerItem.prop('checked')) { |
d1265cbf | 1528 | $containerItem.prop('checked', true); |
5966cc7f | 1529 | this._markedObjectIDs[$type].push($objectID); |
a009a689 | 1530 | $objectIDs.push($objectID); |
0d6ea23f AE |
1531 | } |
1532 | } | |
1533 | else { | |
1a50aae0 | 1534 | if ($containerItem.prop('checked')) { |
d1265cbf | 1535 | $containerItem.prop('checked', false); |
5966cc7f | 1536 | this._markedObjectIDs[$type] = $.removeArrayValue(this._markedObjectIDs[$type], $objectID); |
a009a689 | 1537 | $objectIDs.push($objectID); |
0d6ea23f AE |
1538 | } |
1539 | } | |
a009a689 | 1540 | }, this)); |
483aaa32 AE |
1541 | |
1542 | if ($isMarked) { | |
1543 | $container.find('.jsClipboardObject').addClass('jsMarked'); | |
1544 | } | |
1545 | else { | |
1546 | $container.find('.jsClipboardObject').removeClass('jsMarked'); | |
1547 | } | |
0d6ea23f AE |
1548 | } |
1549 | ||
1550 | // save new status | |
1551 | this._saveState($type, $objectIDs, $isMarked); | |
1552 | }, | |
1553 | ||
1554 | /** | |
1555 | * Saves clipboard item state. | |
1556 | * | |
1557 | * @param string type | |
1558 | * @param array objectIDs | |
1559 | * @param boolean isMarked | |
1560 | */ | |
1561 | _saveState: function(type, objectIDs, isMarked) { | |
1562 | this._proxy.setOption('data', { | |
1563 | action: (isMarked) ? 'mark' : 'unmark', | |
75a93a59 | 1564 | containerData: this._containerData, |
0d6ea23f AE |
1565 | objectIDs: objectIDs, |
1566 | pageClassName: this._page, | |
1f79c900 | 1567 | pageObjectID: this._pageObjectID, |
0d6ea23f | 1568 | type: type |
f4126129 | 1569 | }); |
0d6ea23f AE |
1570 | this._proxy.sendRequest(); |
1571 | }, | |
1572 | ||
1573 | /** | |
1574 | * Updates editor options. | |
1575 | * | |
1576 | * @param object data | |
1577 | * @param string textStatus | |
1578 | * @param jQuery jqXHR | |
1579 | */ | |
1580 | _success: function(data, textStatus, jqXHR) { | |
1581 | // clear all editors first | |
1582 | var $containers = {}; | |
6f475a52 | 1583 | $('.jsClipboardEditor').each(function(index, container) { |
0d6ea23f | 1584 | var $container = $(container); |
1b4e9186 AE |
1585 | var $types = eval($container.data('types')); |
1586 | for (var $i = 0, $length = $types.length; $i < $length; $i++) { | |
1587 | var $typeName = $types[$i]; | |
0d6ea23f AE |
1588 | $containers[$typeName] = $container; |
1589 | } | |
1590 | ||
b3991cb3 AE |
1591 | var $containerID = $container.wcfIdentify(); |
1592 | WCF.CloseOverlayHandler.removeCallback($containerID); | |
ab7de4fd | 1593 | |
0d6ea23f AE |
1594 | $container.empty(); |
1595 | }); | |
1596 | ||
8f3284a3 | 1597 | // do not build new editors |
0d6ea23f AE |
1598 | if (!data.items) return; |
1599 | ||
1600 | // rebuild editors | |
1601 | for (var $typeName in data.items) { | |
1602 | if (!$containers[$typeName]) { | |
1603 | continue; | |
1604 | } | |
1605 | ||
1606 | // create container | |
1607 | var $container = $containers[$typeName]; | |
1b4e9186 AE |
1608 | var $list = $container.children('ul'); |
1609 | if ($list.length == 0) { | |
3ef8dee9 | 1610 | $list = $('<ul />').appendTo($container); |
1b4e9186 AE |
1611 | } |
1612 | ||
0d6ea23f | 1613 | var $editor = data.items[$typeName]; |
3ef8dee9 | 1614 | var $label = $('<li class="dropdown"><span class="dropdownToggle button">' + $editor.label + '</span></li>').appendTo($list); |
844e6560 AE |
1615 | var $itemList = $('<ol class="dropdownMenu"></ol>').appendTo($label); |
1616 | ||
0d6ea23f AE |
1617 | // create editor items |
1618 | for (var $itemIndex in $editor.items) { | |
1619 | var $item = $editor.items[$itemIndex]; | |
49c164a8 | 1620 | |
844e6560 | 1621 | var $listItem = $('<li><span>' + $item.label + '</span></li>').appendTo($itemList); |
6eae9331 | 1622 | $listItem.data('container', $container); |
da27d58a | 1623 | $listItem.data('objectType', $typeName); |
0d6ea23f AE |
1624 | $listItem.data('actionName', $item.actionName).data('parameters', $item.parameters); |
1625 | $listItem.data('internalData', $item.internalData).data('url', $item.url).data('type', $typeName); | |
1626 | ||
1627 | // bind event | |
1628 | $listItem.click($.proxy(this._executeAction, this)); | |
1629 | } | |
9f959ced | 1630 | |
77f8b178 AE |
1631 | // add 'unmark all' |
1632 | $('<li class="dropdownDivider" />').appendTo($itemList); | |
09d6dc28 AE |
1633 | var $foo = $typeName; |
1634 | $('<li><span>' + WCF.Language.get('wcf.clipboard.item.unmarkAll') + '</span></li>').data('typeName', $typeName).appendTo($itemList).click($.proxy(function(event) { | |
1635 | var $typeName = $(event.currentTarget).data('typeName'); | |
1636 | ||
77f8b178 AE |
1637 | this._proxy.setOption('data', { |
1638 | action: 'unmarkAll', | |
1639 | type: $typeName | |
1640 | }); | |
1641 | this._proxy.setOption('success', $.proxy(function(data, textStatus, jqXHR) { | |
2f782339 AE |
1642 | this._containers.each($.proxy(function(index, container) { |
1643 | var $container = $(container); | |
1644 | if ($container.data('type') == $typeName) { | |
1645 | $container.find('.jsClipboardMarkAll, .jsClipboardItem').prop('checked', false); | |
1646 | $container.find('.jsClipboardObject').removeClass('jsMarked'); | |
933a33bb | 1647 | |
2f782339 | 1648 | return false; |
77f8b178 | 1649 | } |
2f782339 | 1650 | }, this)); |
77f8b178 AE |
1651 | |
1652 | // call and restore success method | |
1653 | this._success(data, textStatus, jqXHR); | |
1654 | this._proxy.setOption('success', $.proxy(this._success, this)); | |
09d6dc28 | 1655 | this._loadMarkedItems(); |
77f8b178 AE |
1656 | }, this)); |
1657 | this._proxy.sendRequest(); | |
1658 | }, this)); | |
1659 | ||
3ef8dee9 | 1660 | WCF.Dropdown.initDropdown($label.children('.dropdownToggle'), false); |
0d6ea23f AE |
1661 | } |
1662 | }, | |
da27d58a MS |
1663 | |
1664 | /** | |
1665 | * Closes the clipboard editor item list. | |
1666 | */ | |
b3991cb3 | 1667 | _closeLists: function() { |
d27e2667 | 1668 | $('.jsClipboardEditor ul').removeClass('dropdownOpen'); |
b3991cb3 | 1669 | }, |
0d6ea23f AE |
1670 | |
1671 | /** | |
1672 | * Executes a clipboard editor item action. | |
1673 | * | |
1674 | * @param object event | |
1675 | */ | |
1676 | _executeAction: function(event) { | |
9ce295a0 | 1677 | var $listItem = $(event.currentTarget); |
0d6ea23f AE |
1678 | var $url = $listItem.data('url'); |
1679 | if ($url) { | |
1680 | window.location.href = $url; | |
1681 | } | |
1682 | ||
49c164a8 AE |
1683 | if ($listItem.data('parameters').className && $listItem.data('parameters').actionName) { |
1684 | if ($listItem.data('parameters').actionName === 'unmarkAll' || $listItem.data('parameters').objectIDs) { | |
1685 | var $confirmMessage = $listItem.data('internalData')['confirmMessage']; | |
1686 | if ($confirmMessage) { | |
1687 | var $template = $listItem.data('internalData')['template']; | |
1688 | if ($template) $template = $($template); | |
1689 | ||
1690 | WCF.System.Confirmation.show($confirmMessage, $.proxy(function(action) { | |
1691 | if (action === 'confirm') { | |
1692 | var $data = { }; | |
1693 | ||
1694 | if ($template && $template.length) { | |
1695 | $('#wcfSystemConfirmationContent').find('input, select, textarea').each(function(index, item) { | |
1696 | var $item = $(item); | |
1697 | $data[$item.prop('name')] = $item.val(); | |
1698 | }); | |
1699 | } | |
1700 | ||
1701 | this._executeAJAXActions($listItem, $data); | |
db6b20b8 | 1702 | } |
49c164a8 AE |
1703 | }, this), '', $template); |
1704 | } | |
1705 | else { | |
1706 | this._executeAJAXActions($listItem, { }); | |
1707 | } | |
da27d58a | 1708 | } |
a009a689 MS |
1709 | } |
1710 | ||
0d6ea23f | 1711 | // fire event |
6eae9331 | 1712 | $listItem.data('container').trigger('clipboardAction', [ $listItem.data('type'), $listItem.data('actionName'), $listItem.data('parameters') ]); |
0d6ea23f AE |
1713 | }, |
1714 | ||
da27d58a MS |
1715 | /** |
1716 | * Executes the AJAX actions for the given editor list item. | |
1717 | * | |
1718 | * @param jQuery listItem | |
db6b20b8 | 1719 | * @param object data |
da27d58a | 1720 | */ |
db6b20b8 AE |
1721 | _executeAJAXActions: function(listItem, data) { |
1722 | data = data || { }; | |
1723 | var $objectIDs = []; | |
49c164a8 AE |
1724 | if (listItem.data('parameters').actionName !== 'unmarkAll') { |
1725 | $.each(listItem.data('parameters').objectIDs, function(index, objectID) { | |
1726 | $objectIDs.push(parseInt(objectID)); | |
1727 | }); | |
1728 | } | |
db6b20b8 | 1729 | |
54c91463 | 1730 | var $parameters = { |
438b4f4d MS |
1731 | data: data, |
1732 | containerData: this._containerData[listItem.data('type')] | |
54c91463 | 1733 | }; |
faa1fd02 | 1734 | var $__parameters = listItem.data('internalData')['parameters']; |
54c91463 AE |
1735 | if ($__parameters !== undefined) { |
1736 | for (var $key in $__parameters) { | |
1737 | $parameters[$key] = $__parameters[$key]; | |
1738 | } | |
1739 | } | |
1740 | ||
da27d58a MS |
1741 | new WCF.Action.Proxy({ |
1742 | autoSend: true, | |
1743 | data: { | |
1744 | actionName: listItem.data('parameters').actionName, | |
1745 | className: listItem.data('parameters').className, | |
db6b20b8 | 1746 | objectIDs: $objectIDs, |
54c91463 | 1747 | parameters: $parameters |
da27d58a | 1748 | }, |
1ecfbb69 | 1749 | success: $.proxy(function(data) { |
49c164a8 | 1750 | if (listItem.data('parameters').actionName !== 'unmarkAll') { |
6eae9331 | 1751 | listItem.data('container').trigger('clipboardActionResponse', [ data, listItem.data('type'), listItem.data('actionName'), listItem.data('parameters') ]); |
49c164a8 | 1752 | } |
1ecfbb69 AE |
1753 | |
1754 | this._loadMarkedItems(); | |
91b5b09f | 1755 | }, this) |
da27d58a MS |
1756 | }); |
1757 | ||
1758 | if (this._actionObjects[listItem.data('objectType')] && this._actionObjects[listItem.data('objectType')][listItem.data('parameters').actionName]) { | |
05710b5d | 1759 | this._actionObjects[listItem.data('objectType')][listItem.data('parameters').actionName].triggerEffect($objectIDs); |
da27d58a MS |
1760 | } |
1761 | }, | |
1762 | ||
0d6ea23f AE |
1763 | /** |
1764 | * Sends a clipboard proxy request. | |
1765 | * | |
1766 | * @param object item | |
1767 | */ | |
1768 | sendRequest: function(item) { | |
1769 | var $item = $(item); | |
1770 | ||
1771 | this._actionProxy.setOption('data', { | |
1772 | parameters: $item.data('parameters'), | |
1773 | typeName: $item.data('type') | |
1774 | }); | |
1775 | this._actionProxy.sendRequest(); | |
1776 | } | |
1777 | }; | |
1778 | ||
158bd3ca TD |
1779 | /** |
1780 | * Provides a simple call for periodical executed functions. Based upon | |
1781 | * ideas by Prototype's PeriodicalExecuter. | |
1782 | * | |
1783 | * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/periodical_executer.js | |
1784 | * @param function callback | |
1785 | * @param integer delay | |
1786 | */ | |
39e27190 | 1787 | WCF.PeriodicalExecuter = Class.extend({ |
ac37b8fe AE |
1788 | /** |
1789 | * callback for each execution cycle | |
1790 | * @var object | |
1791 | */ | |
1792 | _callback: null, | |
1793 | ||
ff34ee1f AE |
1794 | /** |
1795 | * interval | |
1796 | * @var integer | |
1797 | */ | |
1798 | _delay: 0, | |
1799 | ||
ac37b8fe AE |
1800 | /** |
1801 | * interval id | |
9f959ced | 1802 | * @var integer |
ac37b8fe AE |
1803 | */ |
1804 | _intervalID: null, | |
1805 | ||
1806 | /** | |
1807 | * execution state | |
1808 | * @var boolean | |
1809 | */ | |
1810 | _isExecuting: false, | |
1811 | ||
158bd3ca TD |
1812 | /** |
1813 | * Initializes a periodical executer. | |
1814 | * | |
1815 | * @param function callback | |
1816 | * @param integer delay | |
1817 | */ | |
1818 | init: function(callback, delay) { | |
ac37b8fe AE |
1819 | if (!$.isFunction(callback)) { |
1820 | console.debug('[WCF.PeriodicalExecuter] Given callback is invalid, aborting.'); | |
1821 | return; | |
1822 | } | |
158bd3ca | 1823 | |
ac37b8fe | 1824 | this._callback = callback; |
ff34ee1f AE |
1825 | this._interval = delay; |
1826 | this.resume(); | |
158bd3ca TD |
1827 | }, |
1828 | ||
1829 | /** | |
1830 | * Executes callback. | |
1831 | */ | |
1832 | _execute: function() { | |
ac37b8fe AE |
1833 | if (!this._isExecuting) { |
1834 | try { | |
1835 | this._isExecuting = true; | |
1836 | this._callback(this); | |
1837 | this._isExecuting = false; | |
1838 | } | |
1839 | catch (e) { | |
1840 | this._isExecuting = false; | |
1841 | throw e; | |
1842 | } | |
158bd3ca TD |
1843 | } |
1844 | }, | |
1845 | ||
1846 | /** | |
1847 | * Terminates loop. | |
1848 | */ | |
1849 | stop: function() { | |
ac37b8fe AE |
1850 | if (!this._intervalID) { |
1851 | return; | |
1852 | } | |
1853 | ||
1854 | clearInterval(this._intervalID); | |
ff34ee1f AE |
1855 | }, |
1856 | ||
1857 | /** | |
1858 | * Resumes the interval-based callback execution. | |
1859 | */ | |
1860 | resume: function() { | |
1861 | if (this._intervalID) { | |
1862 | this.stop(); | |
1863 | } | |
1864 | ||
1865 | this._intervalID = setInterval($.proxy(this._execute, this), this._interval); | |
158bd3ca | 1866 | } |
39e27190 | 1867 | }); |
158bd3ca TD |
1868 | |
1869 | /** | |
093162cc | 1870 | * Handler for loading overlays |
158bd3ca | 1871 | */ |
093162cc | 1872 | WCF.LoadingOverlayHandler = { |
b3991cb3 | 1873 | /** |
093162cc | 1874 | * count of active loading-requests |
b3991cb3 AE |
1875 | * @var integer |
1876 | */ | |
1877 | _activeRequests: 0, | |
9f959ced | 1878 | |
b3991cb3 AE |
1879 | /** |
1880 | * loading overlay | |
1881 | * @var jQuery | |
1882 | */ | |
1883 | _loadingOverlay: null, | |
9f959ced | 1884 | |
526f87f2 AE |
1885 | /** |
1886 | * WCF.PeriodicalExecuter instance | |
1887 | * @var WCF.PeriodicalExecuter | |
1888 | */ | |
1889 | _pending: null, | |
1890 | ||
b3991cb3 | 1891 | /** |
093162cc | 1892 | * Adds one loading-request and shows the loading overlay if nessercery |
b3991cb3 | 1893 | */ |
093162cc MK |
1894 | show: function() { |
1895 | if (this._loadingOverlay === null) { // create loading overlay on first run | |
1be839fc AE |
1896 | this._loadingOverlay = $('<div class="spinner"><span class="icon icon48 icon-spinner" /> <span>' + WCF.Language.get('wcf.global.loading') + '</span></div>').appendTo($('body')); |
1897 | ||
1898 | // fix position | |
1899 | var $width = this._loadingOverlay.outerWidth(); | |
1900 | if ($width < 70) $width = 70; | |
1901 | this._loadingOverlay.css({ | |
1902 | marginLeft: Math.ceil(-1 * $width / 2), | |
1903 | width: $width | |
1904 | }).hide(); | |
093162cc MK |
1905 | } |
1906 | ||
1907 | this._activeRequests++; | |
1908 | if (this._activeRequests == 1) { | |
526f87f2 AE |
1909 | if (this._pending === null) { |
1910 | var self = this; | |
1911 | this._pending = new WCF.PeriodicalExecuter(function(pe) { | |
1912 | if (self._activeRequests) { | |
1913 | self._loadingOverlay.stop(true, true).fadeIn(100); | |
1914 | } | |
1915 | ||
1916 | pe.stop(); | |
1917 | self._pending = null; | |
1918 | }, 250); | |
1919 | } | |
1920 | ||
093162cc MK |
1921 | } |
1922 | }, | |
9f959ced | 1923 | |
b3991cb3 | 1924 | /** |
093162cc MK |
1925 | * Removes one loading-request and hides loading overlay if there're no more pending requests |
1926 | */ | |
1927 | hide: function() { | |
1928 | this._activeRequests--; | |
1929 | if (this._activeRequests == 0) { | |
526f87f2 AE |
1930 | if (this._pending !== null) { |
1931 | this._pending.stop(); | |
1932 | this._pending = null; | |
1933 | } | |
1934 | ||
093162cc MK |
1935 | this._loadingOverlay.stop(true, true).fadeOut(100); |
1936 | } | |
4dbe4dc2 MK |
1937 | }, |
1938 | ||
1939 | /** | |
1940 | * Updates a icon to/from spinner | |
1941 | * | |
1942 | * @param jQuery target | |
1943 | * @pram boolean loading | |
1944 | */ | |
1945 | updateIcon: function(target, loading) { | |
1946 | var $method = (loading === undefined || loading ? 'addClass' : 'removeClass'); | |
1947 | ||
1948 | target.find('.icon')[$method]('icon-spinner'); | |
1949 | if (target.hasClass('icon')) { | |
1950 | target[$method]('icon-spinner'); | |
1951 | } | |
093162cc MK |
1952 | } |
1953 | }; | |
1954 | ||
1955 | /** | |
1956 | * Namespace for AJAXProxies | |
1957 | */ | |
1958 | WCF.Action = {}; | |
1959 | ||
1960 | /** | |
1961 | * Basic implementation for AJAX-based proxyies | |
1962 | * | |
1963 | * @param object options | |
1964 | */ | |
1965 | WCF.Action.Proxy = Class.extend({ | |
906c8f23 AE |
1966 | /** |
1967 | * shows loading overlay for a single request | |
1968 | * @var boolean | |
1969 | */ | |
1970 | _showLoadingOverlayOnce: false, | |
1971 | ||
2f326e85 AE |
1972 | /** |
1973 | * suppresses errors | |
1974 | * @var boolean | |
1975 | */ | |
1976 | _suppressErrors: false, | |
9f959ced | 1977 | |
9ee50dd7 MK |
1978 | /** |
1979 | * last request | |
1980 | * @var jqXHR | |
1981 | */ | |
1982 | _lastRequest: null, | |
1983 | ||
158bd3ca TD |
1984 | /** |
1985 | * Initializes AJAXProxy. | |
1986 | * | |
1987 | * @param object options | |
1988 | */ | |
1989 | init: function(options) { | |
1990 | // initialize default values | |
1991 | this.options = $.extend(true, { | |
1992 | autoSend: false, | |
1993 | data: { }, | |
944d7f97 | 1994 | dataType: 'json', |
158bd3ca TD |
1995 | after: null, |
1996 | init: null, | |
889cdd4c | 1997 | jsonp: 'callback', |
7965fc49 | 1998 | async: true, |
158bd3ca | 1999 | failure: null, |
324e8301 | 2000 | showLoadingOverlay: true, |
158bd3ca | 2001 | success: null, |
88432fe0 | 2002 | suppressErrors: false, |
158bd3ca | 2003 | type: 'POST', |
9ee50dd7 MK |
2004 | url: 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND, |
2005 | aborted: null, | |
2006 | autoAbortPrevious: false | |
158bd3ca TD |
2007 | }, options); |
2008 | ||
2009 | this.confirmationDialog = null; | |
2010 | this.loading = null; | |
906c8f23 | 2011 | this._showLoadingOverlayOnce = false; |
9b1326f4 | 2012 | this._suppressErrors = (this.options.suppressErrors === true); |
158bd3ca TD |
2013 | |
2014 | // send request immediately after initialization | |
2015 | if (this.options.autoSend) { | |
2016 | this.sendRequest(); | |
2017 | } | |
2f326e85 AE |
2018 | |
2019 | var self = this; | |
2020 | $(window).on('beforeunload', function() { self._suppressErrors = true; }); | |
158bd3ca TD |
2021 | }, |
2022 | ||
2023 | /** | |
2024 | * Sends an AJAX request. | |
9ee50dd7 MK |
2025 | * |
2026 | * @param abortPrevious boolean | |
2027 | * @return jqXHR | |
158bd3ca | 2028 | */ |
9ee50dd7 | 2029 | sendRequest: function(abortPrevious) { |
158bd3ca TD |
2030 | this._init(); |
2031 | ||
9ee50dd7 MK |
2032 | if (abortPrevious || this.options.autoAbortPrevious) { |
2033 | this.abortPrevious(); | |
2034 | } | |
2035 | ||
2036 | this._lastRequest = $.ajax({ | |
158bd3ca | 2037 | data: this.options.data, |
944d7f97 | 2038 | dataType: this.options.dataType, |
889cdd4c | 2039 | jsonp: this.options.jsonp, |
7965fc49 | 2040 | async: this.options.async, |
158bd3ca TD |
2041 | type: this.options.type, |
2042 | url: this.options.url, | |
2043 | success: $.proxy(this._success, this), | |
2044 | error: $.proxy(this._failure, this) | |
2045 | }); | |
9ee50dd7 MK |
2046 | return this._lastRequest; |
2047 | }, | |
2048 | ||
2049 | /** | |
2050 | * Aborts the previous request | |
2051 | */ | |
2052 | abortPrevious: function() { | |
2053 | if (this._lastRequest !== null) { | |
2054 | this._lastRequest.abort(); | |
39487b35 | 2055 | this._lastRequest = null; |
9ee50dd7 | 2056 | } |
158bd3ca TD |
2057 | }, |
2058 | ||
906c8f23 AE |
2059 | /** |
2060 | * Shows loading overlay for a single request. | |
2061 | */ | |
2062 | showLoadingOverlayOnce: function() { | |
2063 | this._showLoadingOverlayOnce = true; | |
2064 | }, | |
2065 | ||
88432fe0 AE |
2066 | /** |
2067 | * Suppressed errors for this action proxy. | |
2068 | */ | |
2069 | suppressErrors: function() { | |
2070 | this._suppressErrors = true; | |
2071 | }, | |
2072 | ||
158bd3ca TD |
2073 | /** |
2074 | * Fires before request is send, displays global loading status. | |
2075 | */ | |
2076 | _init: function() { | |
2077 | if ($.isFunction(this.options.init)) { | |
06c820f0 | 2078 | this.options.init(this); |
158bd3ca TD |
2079 | } |
2080 | ||
906c8f23 | 2081 | if (this.options.showLoadingOverlay || this._showLoadingOverlayOnce) { |
093162cc | 2082 | WCF.LoadingOverlayHandler.show(); |
b3991cb3 | 2083 | } |
158bd3ca TD |
2084 | }, |
2085 | ||
2086 | /** | |
2087 | * Handles AJAX errors. | |
2088 | * | |
2089 | * @param object jqXHR | |
2090 | * @param string textStatus | |
2091 | * @param string errorThrown | |
2092 | */ | |
2093 | _failure: function(jqXHR, textStatus, errorThrown) { | |
9ee50dd7 MK |
2094 | if (textStatus == 'abort') { |
2095 | // call child method if applicable | |
2096 | if ($.isFunction(this.options.aborted)) { | |
2097 | this.options.aborted(jqXHR); | |
2098 | } | |
2099 | ||
2100 | return; | |
2101 | } | |
2102 | ||
158bd3ca | 2103 | try { |
c0aaa588 | 2104 | var $data = $.parseJSON(jqXHR.responseText); |
158bd3ca TD |
2105 | |
2106 | // call child method if applicable | |
2f326e85 | 2107 | var $showError = true; |
158bd3ca | 2108 | if ($.isFunction(this.options.failure)) { |
c0aaa588 | 2109 | $showError = this.options.failure($data, jqXHR, textStatus, errorThrown); |
158bd3ca TD |
2110 | } |
2111 | ||
2f326e85 | 2112 | if (!this._suppressErrors && $showError !== false) { |
40f9fe18 AE |
2113 | var $details = ''; |
2114 | if ($data.stacktrace) $details = '<br /><p>Stacktrace:</p><p>' + $data.stacktrace + '</p>'; | |
2115 | else if ($data.exceptionID) $details = '<br /><p>Exception ID: <code>' + $data.exceptionID + '</code></p>'; | |
2116 | ||
2117 | $('<div class="ajaxDebugMessage"><p>' + $data.message + '</p>' + $details + '</div>').wcfDialog({ title: WCF.Language.get('wcf.global.error.title') }); | |
2f326e85 | 2118 | } |
158bd3ca TD |
2119 | } |
2120 | // failed to parse JSON | |
2121 | catch (e) { | |
2f326e85 AE |
2122 | // call child method if applicable |
2123 | var $showError = true; | |
2124 | if ($.isFunction(this.options.failure)) { | |
c0aaa588 | 2125 | $showError = this.options.failure(null, jqXHR, textStatus, errorThrown); |
2f326e85 AE |
2126 | } |
2127 | ||
2128 | if (!this._suppressErrors && $showError !== false) { | |
6ff02d16 | 2129 | var $message = (textStatus === 'timeout') ? WCF.Language.get('wcf.global.error.timeout') : jqXHR.responseText; |
c648d138 AE |
2130 | |
2131 | // validate if $message is neither empty nor 'undefined' | |
2132 | if ($message && $message != 'undefined') { | |
2133 | $('<div class="ajaxDebugMessage"><p>' + $message + '</p></div>').wcfDialog({ title: WCF.Language.get('wcf.global.error.title') }); | |
2134 | } | |
2f326e85 | 2135 | } |
158bd3ca TD |
2136 | } |
2137 | ||
2138 | this._after(); | |
2139 | }, | |
2140 | ||
2141 | /** | |
2142 | * Handles successful AJAX requests. | |
2143 | * | |
2144 | * @param object data | |
2145 | * @param string textStatus | |
2146 | * @param object jqXHR | |
2147 | */ | |
2148 | _success: function(data, textStatus, jqXHR) { | |
2149 | // call child method if applicable | |
2150 | if ($.isFunction(this.options.success)) { | |
9ae57972 | 2151 | // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring |
82b3f032 | 2152 | if (data && data.returnValues && data.returnValues.template !== undefined) { |
9ae57972 AE |
2153 | data.returnValues.template = $.trim(data.returnValues.template); |
2154 | } | |
2155 | ||
158bd3ca TD |
2156 | this.options.success(data, textStatus, jqXHR); |
2157 | } | |
2158 | ||
2159 | this._after(); | |
2160 | }, | |
2161 | ||
2162 | /** | |
2163 | * Fires after an AJAX request, hides global loading status. | |
2164 | */ | |
2165 | _after: function() { | |
9ee50dd7 | 2166 | this._lastRequest = null; |
158bd3ca TD |
2167 | if ($.isFunction(this.options.after)) { |
2168 | this.options.after(); | |
2169 | } | |
350fad79 | 2170 | |
906c8f23 | 2171 | if (this.options.showLoadingOverlay || this._showLoadingOverlayOnce) { |
093162cc | 2172 | WCF.LoadingOverlayHandler.hide(); |
906c8f23 AE |
2173 | |
2174 | if (this._showLoadingOverlayOnce) { | |
2175 | this._showLoadingOverlayOnce = false; | |
2176 | } | |
093162cc MK |
2177 | } |
2178 | ||
42d7d2cc | 2179 | WCF.DOMNodeInsertedHandler.execute(); |
b95fdaea AE |
2180 | |
2181 | // fix anchor tags generated through WCF::getAnchor() | |
2182 | $('a[href*=#]').each(function(index, link) { | |
2183 | var $link = $(link); | |
8111944e | 2184 | if ($link.prop('href').indexOf('AJAXProxy') != -1) { |
b95fdaea AE |
2185 | var $anchor = $link.prop('href').substr($link.prop('href').indexOf('#')); |
2186 | var $pageLink = document.location.toString().replace(/#.*/, ''); | |
2187 | $link.prop('href', $pageLink + $anchor); | |
2188 | } | |
2189 | }); | |
158bd3ca TD |
2190 | }, |
2191 | ||
2192 | /** | |
2193 | * Sets options, MUST be used to set parameters before sending request | |
2194 | * if calling from child classes. | |
2195 | * | |
2196 | * @param string optionName | |
2197 | * @param mixed optionData | |
2198 | */ | |
2199 | setOption: function(optionName, optionData) { | |
2200 | this.options[optionName] = optionData; | |
2201 | } | |
39e27190 | 2202 | }); |
158bd3ca TD |
2203 | |
2204 | /** | |
2205 | * Basic implementation for simple proxy access using bound elements. | |
2206 | * | |
2207 | * @param object options | |
2208 | * @param object callbacks | |
2209 | */ | |
39e27190 | 2210 | WCF.Action.SimpleProxy = Class.extend({ |
158bd3ca TD |
2211 | /** |
2212 | * Initializes SimpleProxy. | |
2213 | * | |
2214 | * @param object options | |
2215 | * @param object callbacks | |
2216 | */ | |
2217 | init: function(options, callbacks) { | |
2218 | /** | |
2219 | * action-specific options | |
2220 | */ | |
2221 | this.options = $.extend(true, { | |
2222 | action: '', | |
2223 | className: '', | |
2224 | elements: null, | |
2225 | eventName: 'click' | |
2226 | }, options); | |
2227 | ||
2228 | /** | |
2229 | * proxy-specific options | |
2230 | */ | |
2231 | this.callbacks = $.extend(true, { | |
2232 | after: null, | |
2233 | failure: null, | |
2234 | init: null, | |
2235 | success: null | |
2236 | }, callbacks); | |
2237 | ||
2238 | if (!this.options.elements) return; | |
2239 | ||
2240 | // initialize proxy | |
2241 | this.proxy = new WCF.Action.Proxy(this.callbacks); | |
2242 | ||
2243 | // bind event listener | |
2244 | this.options.elements.each($.proxy(function(index, element) { | |
2245 | $(element).bind(this.options.eventName, $.proxy(this._handleEvent, this)); | |
2246 | }, this)); | |
2247 | }, | |
2248 | ||
2249 | /** | |
2250 | * Handles event actions. | |
2251 | * | |
2252 | * @param object event | |
2253 | */ | |
2254 | _handleEvent: function(event) { | |
2255 | this.proxy.setOption('data', { | |
2256 | actionName: this.options.action, | |
2257 | className: this.options.className, | |
2258 | objectIDs: [ $(event.target).data('objectID') ] | |
2259 | }); | |
2260 | ||
2261 | this.proxy.sendRequest(); | |
2262 | } | |
39e27190 | 2263 | }); |
158bd3ca TD |
2264 | |
2265 | /** | |
2266 | * Basic implementation for AJAXProxy-based deletion. | |
2267 | * | |
2268 | * @param string className | |
d371330f | 2269 | * @param string containerSelector |
9d4465dd | 2270 | * @param string buttonSelector |
158bd3ca | 2271 | */ |
5ffc72b6 | 2272 | WCF.Action.Delete = Class.extend({ |
9d4465dd AE |
2273 | /** |
2274 | * delete button selector | |
2275 | * @var string | |
2276 | */ | |
2277 | _buttonSelector: '', | |
2278 | ||
d371330f AE |
2279 | /** |
2280 | * action class name | |
2281 | * @var string | |
2282 | */ | |
2283 | _className: '', | |
2284 | ||
2285 | /** | |
2286 | * container selector | |
2287 | * @var string | |
2288 | */ | |
2289 | _containerSelector: '', | |
2290 | ||
2291 | /** | |
2292 | * list of known container ids | |
2293 | * @var array<string> | |
2294 | */ | |
2295 | _containers: [ ], | |
2296 | ||
158bd3ca TD |
2297 | /** |
2298 | * Initializes 'delete'-Proxy. | |
2299 | * | |
2300 | * @param string className | |
d371330f | 2301 | * @param string containerSelector |
9d4465dd | 2302 | * @param string buttonSelector |
158bd3ca | 2303 | */ |
9d4465dd | 2304 | init: function(className, containerSelector, buttonSelector) { |
d371330f AE |
2305 | this._containerSelector = containerSelector; |
2306 | this._className = className; | |
9d4465dd AE |
2307 | this._buttonSelector = (buttonSelector) ? buttonSelector : '.jsDeleteButton'; |
2308 | ||
d371330f | 2309 | this.proxy = new WCF.Action.Proxy({ |
158bd3ca | 2310 | success: $.proxy(this._success, this) |
d371330f | 2311 | }); |
158bd3ca | 2312 | |
d371330f AE |
2313 | this._initElements(); |
2314 | ||
2315 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Action.Delete' + this._className.hashCode(), $.proxy(this._initElements, this)); | |
2316 | }, | |
2317 | ||
2318 | /** | |
2319 | * Initializes available element containers. | |
2320 | */ | |
2321 | _initElements: function() { | |
2322 | var self = this; | |
2323 | $(this._containerSelector).each(function(index, container) { | |
2324 | var $container = $(container); | |
2325 | var $containerID = $container.wcfIdentify(); | |
2326 | ||
2327 | if (!WCF.inArray($containerID, self._containers)) { | |
2328 | self._containers.push($containerID); | |
4ae603a5 | 2329 | $container.find(self._buttonSelector).click($.proxy(self._click, self)); |
d371330f AE |
2330 | } |
2331 | }); | |
158bd3ca TD |
2332 | }, |
2333 | ||
2334 | /** | |
2335 | * Sends AJAX request. | |
2336 | * | |
2337 | * @param object event | |
2338 | */ | |
2339 | _click: function(event) { | |
eea52a29 | 2340 | var $target = $(event.currentTarget); |
b437bee4 | 2341 | event.preventDefault(); |
158bd3ca TD |
2342 | |
2343 | if ($target.data('confirmMessage')) { | |
f02b3f75 | 2344 | WCF.System.Confirmation.show($target.data('confirmMessage'), $.proxy(this._execute, this), { target: $target }); |
158bd3ca TD |
2345 | } |
2346 | else { | |
4dbe4dc2 | 2347 | WCF.LoadingOverlayHandler.updateIcon($target); |
158bd3ca TD |
2348 | this._sendRequest($target); |
2349 | } | |
f02b3f75 AE |
2350 | }, |
2351 | ||
90a29960 MS |
2352 | /** |
2353 | * Is called if the delete effect has been triggered on the given element. | |
2354 | * | |
2355 | * @param jQuery element | |
2356 | */ | |
2357 | _didTriggerEffect: function(element) { | |
2358 | // does nothing | |
2359 | }, | |
2360 | ||
f02b3f75 AE |
2361 | /** |
2362 | * Executes deletion. | |
2363 | * | |
2364 | * @param string action | |
2365 | * @param object parameters | |
2366 | */ | |
2367 | _execute: function(action, parameters) { | |
2368 | if (action === 'cancel') { | |
2369 | return; | |
2370 | } | |
158bd3ca | 2371 | |
4dbe4dc2 | 2372 | WCF.LoadingOverlayHandler.updateIcon(parameters.target); |
f02b3f75 | 2373 | this._sendRequest(parameters.target); |
158bd3ca TD |
2374 | }, |
2375 | ||
ca42ad27 MK |
2376 | /** |
2377 | * Sends the request | |
2378 | * | |
2379 | * @param jQuery object | |
2380 | */ | |
158bd3ca TD |
2381 | _sendRequest: function(object) { |
2382 | this.proxy.setOption('data', { | |
2383 | actionName: 'delete', | |
d371330f | 2384 | className: this._className, |
f005c2c9 | 2385 | interfaceName: 'wcf\\data\\IDeleteAction', |
158bd3ca TD |
2386 | objectIDs: [ $(object).data('objectID') ] |
2387 | }); | |
2388 | ||
2389 | this.proxy.sendRequest(); | |
2390 | }, | |
2391 | ||
2392 | /** | |
2393 | * Deletes items from containers. | |
2394 | * | |
2395 | * @param object data | |
2396 | * @param string textStatus | |
2397 | * @param object jqXHR | |
2398 | */ | |
2399 | _success: function(data, textStatus, jqXHR) { | |
da27d58a MS |
2400 | this.triggerEffect(data.objectIDs); |
2401 | }, | |
2402 | ||
2403 | /** | |
2404 | * Triggers the delete effect for the objects with the given ids. | |
2405 | * | |
2406 | * @param array objectIDs | |
2407 | */ | |
2408 | triggerEffect: function(objectIDs) { | |
d371330f AE |
2409 | for (var $index in this._containers) { |
2410 | var $container = $('#' + this._containers[$index]); | |
02a8ca7a | 2411 | if (WCF.inArray($container.find(this._buttonSelector).data('objectID'), objectIDs)) { |
90a29960 MS |
2412 | var self = this; |
2413 | $container.wcfBlindOut('up',function() { | |
2414 | $(this).remove(); | |
2415 | self._containers.splice(self._containers.indexOf($(this).wcfIdentify()), 1); | |
2416 | self._didTriggerEffect($(this)); | |
2417 | }); | |
158bd3ca | 2418 | } |
d371330f | 2419 | } |
158bd3ca | 2420 | } |
5ffc72b6 | 2421 | }); |
158bd3ca | 2422 | |
8a52f0a4 MS |
2423 | /** |
2424 | * Basic implementation for deletion of nested elements. | |
2425 | * | |
2426 | * The implementation requires the nested elements to be grouped as numbered lists | |
2427 | * (ol lists). The child elements of the deleted elements are moved to the parent | |
2428 | * element of the deleted element. | |
2429 | * | |
2430 | * @see WCF.Action.Delete | |
2431 | */ | |
2432 | WCF.Action.NestedDelete = WCF.Action.Delete.extend({ | |
2433 | /** | |
2434 | * @see WCF.Action.Delete.triggerEffect() | |
2435 | */ | |
2436 | triggerEffect: function(objectIDs) { | |
2437 | for (var $index in this._containers) { | |
2438 | var $container = $('#' + this._containers[$index]); | |
2439 | if (WCF.inArray($container.find(this._buttonSelector).data('objectID'), objectIDs)) { | |
90a29960 MS |
2440 | // move children up |
2441 | if ($container.has('ol').has('li').length) { | |
8a52f0a4 MS |
2442 | if ($container.is(':only-child')) { |
2443 | $container.parent().replaceWith($container.find('> ol')); | |
2444 | } | |
2445 | else { | |
2446 | $container.replaceWith($container.find('> ol > li')); | |
2447 | } | |
90a29960 MS |
2448 | |
2449 | this._containers.splice(this._containers.indexOf($container.wcfIdentify()), 1); | |
2450 | this._didTriggerEffect($container); | |
8a52f0a4 MS |
2451 | } |
2452 | else { | |
90a29960 MS |
2453 | var self = this; |
2454 | $container.wcfBlindOut('up', function() { | |
2455 | $(this).remove(); | |
2456 | self._containers.splice(self._containers.indexOf($(this).wcfIdentify()), 1); | |
2457 | self._didTriggerEffect($(this)); | |
2458 | }); | |
8a52f0a4 MS |
2459 | } |
2460 | } | |
2461 | } | |
2462 | } | |
2463 | }); | |
2464 | ||
158bd3ca TD |
2465 | /** |
2466 | * Basic implementation for AJAXProxy-based toggle actions. | |
2467 | * | |
2468 | * @param string className | |
2469 | * @param jQuery containerList | |
9d4465dd | 2470 | * @param string buttonSelector |
158bd3ca | 2471 | */ |
5ffc72b6 | 2472 | WCF.Action.Toggle = Class.extend({ |
9d4465dd AE |
2473 | /** |
2474 | * toogle button selector | |
2475 | * @var string | |
2476 | */ | |
2477 | _buttonSelector: '.jsToggleButton', | |
2478 | ||
5dd8bc73 MK |
2479 | /** |
2480 | * action class name | |
2481 | * @var string | |
2482 | */ | |
2483 | _className: '', | |
2484 | ||
2485 | /** | |
2486 | * container selector | |
2487 | * @var string | |
2488 | */ | |
2489 | _containerSelector: '', | |
2490 | ||
2491 | /** | |
2492 | * list of known container ids | |
2493 | * @var array<string> | |
2494 | */ | |
2495 | _containers: [ ], | |
2496 | ||
158bd3ca TD |
2497 | /** |
2498 | * Initializes 'toggle'-Proxy | |
2499 | * | |
2500 | * @param string className | |
5dd8bc73 | 2501 | * @param string containerSelector |
9d4465dd | 2502 | * @param string buttonSelector |
158bd3ca | 2503 | */ |
9d4465dd | 2504 | init: function(className, containerSelector, buttonSelector) { |
5dd8bc73 MK |
2505 | this._containerSelector = containerSelector; |
2506 | this._className = className; | |
9d4465dd | 2507 | this._buttonSelector = (buttonSelector) ? buttonSelector : '.jsToggleButton'; |
5f6f59eb | 2508 | this._containers = [ ]; |
32a9e8a4 | 2509 | |
158bd3ca TD |
2510 | // initialize proxy |
2511 | var options = { | |
2512 | success: $.proxy(this._success, this) | |
2513 | }; | |
2514 | this.proxy = new WCF.Action.Proxy(options); | |
2515 | ||
2516 | // bind event listener | |
46fc9d49 MK |
2517 | this._initElements(); |
2518 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Action.Toggle' + this._className.hashCode(), $.proxy(this._initElements, this)); | |
5dd8bc73 MK |
2519 | }, |
2520 | ||
2521 | /** | |
2522 | * Initializes available element containers. | |
2523 | */ | |
2524 | _initElements: function() { | |
2525 | $(this._containerSelector).each($.proxy(function(index, container) { | |
2526 | var $container = $(container); | |
2527 | var $containerID = $container.wcfIdentify(); | |
2528 | ||
2529 | if (!WCF.inArray($containerID, this._containers)) { | |
2530 | this._containers.push($containerID); | |
9d4465dd | 2531 | $container.find(this._buttonSelector).click($.proxy(this._click, this)); |
5dd8bc73 | 2532 | } |
158bd3ca TD |
2533 | }, this)); |
2534 | }, | |
2535 | ||
2536 | /** | |
2537 | * Sends AJAX request. | |
2538 | * | |
2539 | * @param object event | |
2540 | */ | |
2541 | _click: function(event) { | |
fc8ba59a | 2542 | var $target = $(event.currentTarget); |
b437bee4 | 2543 | event.preventDefault(); |
fc8ba59a MK |
2544 | |
2545 | if ($target.data('confirmMessage')) { | |
2546 | WCF.System.Confirmation.show($target.data('confirmMessage'), $.proxy(this._execute, this), { target: $target }); | |
2547 | } | |
2548 | else { | |
4dbe4dc2 | 2549 | WCF.LoadingOverlayHandler.updateIcon($target); |
fc8ba59a MK |
2550 | this._sendRequest($target); |
2551 | } | |
2552 | }, | |
2553 | ||
2554 | /** | |
2555 | * Executes toggeling. | |
2556 | * | |
2557 | * @param string action | |
2558 | * @param object parameters | |
2559 | */ | |
2560 | _execute: function(action, parameters) { | |
2561 | if (action === 'cancel') { | |
2562 | return; | |
2563 | } | |
2564 | ||
4dbe4dc2 | 2565 | WCF.LoadingOverlayHandler.updateIcon(parameters.target); |
fc8ba59a MK |
2566 | this._sendRequest(parameters.target); |
2567 | }, | |
2568 | ||
2569 | _sendRequest: function(object) { | |
158bd3ca TD |
2570 | this.proxy.setOption('data', { |
2571 | actionName: 'toggle', | |
5dd8bc73 | 2572 | className: this._className, |
f005c2c9 | 2573 | interfaceName: 'wcf\\data\\IToggleAction', |
fc8ba59a | 2574 | objectIDs: [ $(object).data('objectID') ] |
158bd3ca TD |
2575 | }); |
2576 | ||
2577 | this.proxy.sendRequest(); | |
2578 | }, | |
2579 | ||
2580 | /** | |
2581 | * Toggles status icons. | |
2582 | * | |
2583 | * @param object data | |
2584 | * @param string textStatus | |
2585 | * @param object jqXHR | |
2586 | */ | |
2587 | _success: function(data, textStatus, jqXHR) { | |
da27d58a MS |
2588 | this.triggerEffect(data.objectIDs); |
2589 | }, | |
2590 | ||
2591 | /** | |
2592 | * Triggers the toggle effect for the objects with the given ids. | |
2593 | * | |
2594 | * @param array objectIDs | |
2595 | */ | |
2596 | triggerEffect: function(objectIDs) { | |
5dd8bc73 MK |
2597 | for (var $index in this._containers) { |
2598 | var $container = $('#' + this._containers[$index]); | |
9d4465dd | 2599 | var $toggleButton = $container.find(this._buttonSelector); |
da27d58a | 2600 | if (WCF.inArray($toggleButton.data('objectID'), objectIDs)) { |
f6e4854a MK |
2601 | $container.wcfHighlight(); |
2602 | this._toggleButton($container, $toggleButton); | |
158bd3ca | 2603 | } |
5dd8bc73 | 2604 | } |
f6e4854a MK |
2605 | }, |
2606 | ||
2607 | /** | |
2608 | * Tiggers the toggle effect on a button | |
2609 | * | |
2610 | * @param jQuery $container | |
2611 | * @param jQuery $toggleButton | |
2612 | */ | |
2613 | _toggleButton: function($container, $toggleButton) { | |
2614 | // toggle icon source | |
4dbe4dc2 | 2615 | WCF.LoadingOverlayHandler.updateIcon($toggleButton, false); |
d3bd4037 MW |
2616 | if ($toggleButton.hasClass('icon-check-empty')) { |
2617 | $toggleButton.removeClass('icon-check-empty').addClass('icon-check'); | |
4aa6daef | 2618 | $newTitle = ($toggleButton.data('disableTitle') ? $toggleButton.data('disableTitle') : WCF.Language.get('wcf.global.button.disable')); |
556973c1 MW |
2619 | $toggleButton.attr('title', $newTitle); |
2620 | } | |
2621 | else { | |
d3bd4037 | 2622 | $toggleButton.removeClass('icon-check').addClass('icon-check-empty'); |
4aa6daef | 2623 | $newTitle = ($toggleButton.data('enableTitle') ? $toggleButton.data('enableTitle') : WCF.Language.get('wcf.global.button.enable')); |
556973c1 MW |
2624 | $toggleButton.attr('title', $newTitle); |
2625 | } | |
2626 | ||
f6e4854a MK |
2627 | // toggle css class |
2628 | $container.toggleClass('disabled'); | |
158bd3ca | 2629 | } |
5ffc72b6 | 2630 | }); |
158bd3ca | 2631 | |
aa4fb64e AE |
2632 | /** |
2633 | * Executes provided callback if scroll threshold is reached. Usuable to determine | |
2634 | * if user reached the bottom of an element to load new elements on the fly. | |
2635 | * | |
2636 | * If you do not provide a value for 'reference' and 'target' it will assume you're | |
2637 | * monitoring page scrolls, otherwise a valid jQuery selector must be provided for both. | |
2638 | * | |
2639 | * @param integer threshold | |
2640 | * @param object callback | |
2641 | * @param string reference | |
2642 | * @param string target | |
2643 | */ | |
2644 | WCF.Action.Scroll = Class.extend({ | |
2645 | /** | |
2646 | * callback used once threshold is reached | |
2647 | * @var object | |
2648 | */ | |
2649 | _callback: null, | |
2650 | ||
2651 | /** | |
2652 | * reference object | |
2653 | * @var jQuery | |
2654 | */ | |
2655 | _reference: null, | |
2656 | ||
2657 | /** | |
2658 | * target object | |
2659 | * @var jQuery | |
2660 | */ | |
2661 | _target: null, | |
2662 | ||
2663 | /** | |
2664 | * threshold value | |
2665 | * @var integer | |
2666 | */ | |
2667 | _threshold: 0, | |
2668 | ||
2669 | /** | |
2670 | * Initializes a new WCF.Action.Scroll object. | |
2671 | * | |
2672 | * @param integer threshold | |
2673 | * @param object callback | |
2674 | * @param string reference | |
2675 | * @param string target | |
2676 | */ | |
2677 | init: function(threshold, callback, reference, target) { | |
2678 | this._threshold = parseInt(threshold); | |
2679 | if (this._threshold === 0) { | |
2680 | console.debug("[WCF.Action.Scroll] Given threshold is invalid, aborting."); | |
2681 | return; | |
2682 | } | |
2683 | ||
2684 | if ($.isFunction(callback)) this._callback = callback; | |
2685 | if (this._callback === null) { | |
2686 | console.debug("[WCF.Action.Scroll] Given callback is invalid, aborting."); | |
2687 | return; | |
2688 | } | |
2689 | ||
2690 | // bind element references | |
2691 | this._reference = $((reference) ? reference : window); | |
2692 | this._target = $((target) ? target : document); | |
2693 | ||
2694 | // watch for scroll event | |
2695 | this.start(); | |
5cc5eff0 AE |
2696 | |
2697 | // check if browser navigated back and jumped to offset before JavaScript was loaded | |
2698 | this._scroll(); | |
aa4fb64e AE |
2699 | }, |
2700 | ||
2701 | /** | |
2702 | * Calculates if threshold is reached and notifies callback. | |
2703 | */ | |
2704 | _scroll: function() { | |
2705 | var $targetHeight = this._target.height(); | |
2706 | var $topOffset = this._reference.scrollTop(); | |
2707 | var $referenceHeight = this._reference.height(); | |
2708 | ||
2709 | // calculate if defined threshold is visible | |
2710 | if (($targetHeight - ($referenceHeight + $topOffset)) < this._threshold) { | |
2711 | this._callback(this); | |
2712 | } | |
2713 | }, | |
2714 | ||
2715 | /** | |
2716 | * Enables scroll monitoring, may be used to resume. | |
2717 | */ | |
2718 | start: function() { | |
2719 | this._reference.on('scroll', $.proxy(this._scroll, this)); | |
221fce41 | 2720 | }, |
aa4fb64e AE |
2721 | |
2722 | /** | |
2723 | * Disables scroll monitoring, e.g. no more elements loadable. | |
2724 | */ | |
2725 | stop: function() { | |
2726 | this._reference.off('scroll'); | |
2727 | } | |
2728 | }); | |
2729 | ||
158bd3ca TD |
2730 | /** |
2731 | * Namespace for date-related functions. | |
2732 | */ | |
2733 | WCF.Date = {}; | |
2734 | ||
81f55d8f AE |
2735 | /** |
2736 | * Provides a date picker for date input fields. | |
2737 | */ | |
2738 | WCF.Date.Picker = { | |
645231ef AE |
2739 | /** |
2740 | * date format | |
2741 | * @var string | |
2742 | */ | |
6af5fef6 DR |
2743 | _dateFormat: 'yy-mm-dd', |
2744 | ||
b418da85 AE |
2745 | /** |
2746 | * time format | |
2747 | * @var string | |
2748 | */ | |
2749 | _timeFormat: 'g:ia', | |
2750 | ||
81f55d8f AE |
2751 | /** |
2752 | * Initializes the jQuery UI based date picker. | |
2753 | */ | |
2754 | init: function() { | |
b418da85 AE |
2755 | // ignore error 'unexpected literal' error; this might be not the best approach |
2756 | // to fix this problem, but since the date is properly processed anyway, we can | |
2757 | // simply continue :) - Alex | |
2758 | var $__log = $.timepicker.log; | |
2759 | $.timepicker.log = function(error) { | |
65dd3bae | 2760 | if (error.indexOf('Error parsing the date/time string: Unexpected literal at position') == -1 && error.indexOf('Error parsing the date/time string: Unknown name at position') == -1) { |
b418da85 AE |
2761 | $__log(error); |
2762 | } | |
2763 | }; | |
2764 | ||
6af5fef6 | 2765 | this._convertDateFormat(); |
c5286020 | 2766 | this._initDatePicker(); |
c5286020 AE |
2767 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Date.Picker', $.proxy(this._initDatePicker, this)); |
2768 | }, | |
2769 | ||
6af5fef6 DR |
2770 | /** |
2771 | * Convert PHPs date() format to jQuery UIs date picker format. | |
2772 | */ | |
2773 | _convertDateFormat: function() { | |
6af5fef6 DR |
2774 | // replacement table |
2775 | // format of PHP date() => format of jQuery UI date picker | |
2776 | // | |
2777 | // No equivalence in PHP date(): | |
2778 | // oo day of the year (three digit) | |
2779 | // ! Windows ticks (100ns since 01/01/0001) | |
2780 | // | |
2781 | // No equivalence in jQuery UI date picker: | |
2782 | // N ISO-8601 numeric representation of the day of the week | |
6af5fef6 DR |
2783 | // w Numeric representation of the day of the week |
2784 | // W ISO-8601 week number of year, weeks starting on Monday | |
2785 | // t Number of days in the given month | |
2786 | // L Whether it's a leap year | |
d83154c3 | 2787 | var $replacementTable = { |
b418da85 | 2788 | // time |
65dd3bae AE |
2789 | 'a': 'tt', |
2790 | 'A': 'TT', | |
b418da85 AE |
2791 | 'g': 'h', |
2792 | 'G': 'H', | |
2793 | 'h': 'hh', | |
2794 | 'H': 'HH', | |
2795 | 'i': 'mm', | |
2796 | 's': 'ss', | |
2797 | 'u': 'l', | |
2798 | ||
6af5fef6 DR |
2799 | // day |
2800 | 'd': 'dd', | |
2801 | 'D': 'D', | |
2802 | 'j': 'd', | |
2803 | 'l': 'DD', | |
2804 | 'z': 'o', | |
5e4cf40e | 2805 | 'S': '', // English ordinal suffix for the day of the month, 2 characters, will be discarded |
6af5fef6 DR |
2806 | |
2807 | // month | |
2808 | 'F': 'MM', | |
2809 | 'm': 'mm', | |
2810 | 'M': 'M', | |
2811 | 'n': 'm', | |
2812 | ||
2813 | // year | |
2814 | 'o': 'yy', | |
2815 | 'Y': 'yy', | |
2816 | 'y': 'y', | |
2817 | ||
2818 | // timestamp | |
2819 | 'U': '@' | |
2820 | }; | |
2821 | ||
2822 | // do the actual replacement | |
2823 | // this is not perfect, but a basic implementation and should work in 99% of the cases | |
5e4cf40e | 2824 | this._dateFormat = WCF.Language.get('wcf.date.dateFormat').replace(/([^dDjlzSFmMnoYyU\\]*(?:\\.[^dDjlzSFmMnoYyU\\]*)*)([dDjlzSFmMnoYyU])/g, function(match, part1, part2, offset, string) { |
d83154c3 AE |
2825 | for (var $key in $replacementTable) { |
2826 | if (part2 == $key) { | |
2827 | part2 = $replacementTable[$key]; | |
6af5fef6 | 2828 | } |
d83154c3 AE |
2829 | } |
2830 | ||
6af5fef6 DR |
2831 | return part1 + part2; |
2832 | }); | |
b418da85 AE |
2833 | |
2834 | this._timeFormat = WCF.Language.get('wcf.date.timeFormat').replace(/([^aAgGhHisu\\]*(?:\\.[^aAgGhHisu\\]*)*)([aAgGhHisu])/g, function(match, part1, part2, offset, string) { | |
2835 | for (var $key in $replacementTable) { | |
2836 | if (part2 == $key) { | |
2837 | part2 = $replacementTable[$key]; | |
2838 | } | |
2839 | } | |
2840 | ||
2841 | return part1 + part2; | |
2842 | }); | |
6af5fef6 DR |
2843 | }, |
2844 | ||
c5286020 AE |
2845 | /** |
2846 | * Initializes the date picker for valid fields. | |
2847 | */ | |
2848 | _initDatePicker: function() { | |
b9e59fc3 | 2849 | $('input[type=date]:not(.jsDatePicker), input[type=datetime]:not(.jsDatePicker)').each($.proxy(function(index, input) { |
4209cfa0 | 2850 | var $input = $(input); |
d83154c3 | 2851 | var $inputName = $input.prop('name'); |
b9e59fc3 MS |
2852 | var $inputValue = $input.val(); // should be Y-m-d (H:i:s), must be interpretable by Date |
2853 | ||
2854 | var $hasTime = $input.attr('type') == 'datetime'; | |
81f55d8f | 2855 | |
4209cfa0 DR |
2856 | // update $input |
2857 | $input.prop('type', 'text').addClass('jsDatePicker'); | |
2858 | ||
86db109f MW |
2859 | // set placeholder |
2860 | if ($input.data('placeholder')) $input.attr('placeholder', $input.data('placeholder')); | |
2861 | ||
4209cfa0 | 2862 | // insert a hidden element representing the actual date |
d83154c3 AE |
2863 | $input.removeAttr('name'); |
2864 | $input.before('<input type="hidden" id="' + $input.wcfIdentify() + 'DatePicker" name="' + $inputName + '" value="' + $inputValue + '" />'); | |
81f55d8f | 2865 | |
b9e59fc3 MS |
2866 | // max- and mindate |
2867 | var $maxDate = $input.attr('max') ? new Date($input.attr('max').replace(' ', 'T')) : null; | |
2868 | var $minDate = $input.attr('min') ? new Date($input.attr('min').replace(' ', 'T')) : null; | |
2869 | ||
4209cfa0 | 2870 | // init date picker |
b9e59fc3 | 2871 | $options = { |
d83154c3 AE |
2872 | altField: '#' + $input.wcfIdentify() + 'DatePicker', |
2873 | altFormat: 'yy-mm-dd', // PHPs strtotime() understands this best | |
5a1293b4 AE |
2874 | beforeShow: function(input, instance) { |
2875 | // dirty hack to force opening below the input | |
2876 | setTimeout(function() { | |
2877 | instance.dpDiv.position({ | |
2878 | my: 'left top', | |
2879 | at: 'left bottom', | |
2880 | collision: 'none', | |
2881 | of: input | |
2882 | }); | |
2883 | }, 1); | |
2884 | }, | |
81f55d8f AE |
2885 | changeMonth: true, |
2886 | changeYear: true, | |
6af5fef6 | 2887 | dateFormat: this._dateFormat, |
6af5fef6 DR |
2888 | dayNames: WCF.Language.get('__days'), |
2889 | dayNamesMin: WCF.Language.get('__daysShort'), | |
2890 | dayNamesShort: WCF.Language.get('__daysShort'), | |
9f8d4e52 | 2891 | firstDay: parseInt(WCF.Language.get('wcf.date.firstDayOfTheWeek')) || 0, |
b9e59fc3 MS |
2892 | isRTL: WCF.Language.get('wcf.global.pageDirection') == 'rtl', |
2893 | maxDate: $maxDate, | |
2894 | minDate: $minDate, | |
6af5fef6 | 2895 | monthNames: WCF.Language.get('__months'), |
d83154c3 | 2896 | monthNamesShort: WCF.Language.get('__monthsShort'), |
b9e59fc3 | 2897 | showButtonPanel: false, |
25bed976 TD |
2898 | onClose: function(dateText, datePicker) { |
2899 | // clear altField when datepicker is cleared | |
2900 | if (dateText == '') { | |
2901 | $(datePicker.settings["altField"]).val(dateText); | |
2902 | } | |
b9e59fc3 MS |
2903 | }, |
2904 | showOtherMonths: true, | |
2905 | yearRange: ($input.hasClass('birthday') ? '-100:+0' : '1900:2038') | |
2906 | }; | |
4209cfa0 | 2907 | |
b9e59fc3 MS |
2908 | if ($hasTime) { |
2909 | // drop the seconds | |
2910 | if (/[0-9]{2}:[0-9]{2}:[0-9]{2}$/.test($inputValue)) { | |
2911 | $inputValue = $inputValue.replace(/:[0-9]{2}$/, ''); | |
2912 | $input.val($inputValue); | |
2913 | } | |
2914 | $inputValue = $inputValue.replace(' ', 'T'); | |
2915 | ||
d51dd25a | 2916 | if ($input.data('ignoreTimezone')) { |
18d20401 | 2917 | var $timezoneOffset = new Date($inputValue).getTimezoneOffset(); |
d51dd25a AE |
2918 | var $timezone = ($timezoneOffset > 0) ? '-' : '+'; // -120 equals GMT+0200 |
2919 | $timezoneOffset = Math.abs($timezoneOffset); | |
2920 | var $hours = (Math.floor($timezoneOffset / 60)).toString(); | |
2921 | var $minutes = ($timezoneOffset % 60).toString(); | |
2922 | $timezone += ($hours.length == 2) ? $hours : '0' + $hours; | |
2923 | $timezone += ':'; | |
2924 | $timezone += ($minutes.length == 2) ? $minutes : '0' + $minutes; | |
2925 | ||
2926 | $inputValue = $inputValue.replace(/[+-][0-9]{2}:[0-9]{2}$/, $timezone); | |
2927 | } | |
2928 | ||
b9e59fc3 MS |
2929 | $options = $.extend($options, { |
2930 | altFieldTimeOnly: false, | |
2931 | altTimeFormat: 'HH:mm', | |
2932 | controlType: 'select', | |
2933 | hourText: WCF.Language.get('wcf.date.hour'), | |
2934 | minuteText: WCF.Language.get('wcf.date.minute'), | |
2935 | showTime: false, | |
2936 | timeFormat: this._timeFormat, | |
2937 | yearRange: ($input.hasClass('birthday') ? '-100:+0' : '1900:2038') | |
2938 | }); | |
645231ef | 2939 | } |
4209cfa0 | 2940 | |
b9e59fc3 MS |
2941 | if ($hasTime) { |
2942 | $input.datetimepicker($options); | |
2943 | } | |
2944 | else { | |
2945 | $input.datepicker($options); | |
b418da85 | 2946 | } |
b418da85 AE |
2947 | |
2948 | // format default date | |
2949 | if ($inputValue) { | |
f5dfcd05 AE |
2950 | if (!$hasTime) { |
2951 | // drop timezone for date-only input | |
2952 | $inputValue = new Date($inputValue); | |
2953 | $inputValue.setMinutes($inputValue.getMinutes() + $inputValue.getTimezoneOffset()); | |
2954 | } | |
2955 | ||
b9e59fc3 | 2956 | $input.datepicker('setDate', $inputValue); |
b418da85 AE |
2957 | } |
2958 | ||
2959 | // bug workaround: setDate creates the widget but unfortunately doesn't hide it... | |
2960 | $input.datepicker('widget').hide(); | |
2961 | }, this)); | |
81f55d8f AE |
2962 | } |
2963 | }; | |
2964 | ||
158bd3ca TD |
2965 | /** |
2966 | * Provides utility functions for date operations. | |
2967 | */ | |
2968 | WCF.Date.Util = { | |
2969 | /** | |
2970 | * Returns UTC timestamp, if date is not given, current time will be used. | |
2971 | * | |
2972 | * @param Date date | |
2973 | * @return integer | |
2974 | */ | |
2975 | gmdate: function(date) { | |
2976 | var $date = (date) ? date : new Date(); | |
2977 | ||
2978 | return Math.round(Date.UTC( | |
2979 | $date.getUTCFullYear(), | |
2980 | $date.getUTCMonth(), | |
2981 | $date.getUTCDay(), | |
2982 | $date.getUTCHours(), | |
2983 | $date.getUTCMinutes(), | |
2984 | $date.getUTCSeconds() | |
2985 | ) / 1000); | |
2986 | }, | |
2987 | ||
2988 | /** | |
2989 | * Returns a Date object with precise offset (including timezone and local timezone). | |
88ff183f | 2990 | * Parameters timestamp and offset must be in miliseconds! |
158bd3ca TD |
2991 | * |
2992 | * @param integer timestamp | |
2993 | * @param integer offset | |
2994 | * @return Date | |
2995 | */ | |
2996 | getTimezoneDate: function(timestamp, offset) { | |
2997 | var $date = new Date(timestamp); | |
88ff183f | 2998 | var $localOffset = $date.getTimezoneOffset() * 60000; |
158bd3ca | 2999 | |
88ff183f | 3000 | return new Date((timestamp + $localOffset + offset)); |
158bd3ca TD |
3001 | } |
3002 | }; | |
3003 | ||
3004 | /** | |
3005 | * Handles relative time designations. | |
3006 | */ | |
81f55d8f | 3007 | WCF.Date.Time = Class.extend({ |
9cf417b1 MS |
3008 | /** |
3009 | * Date of current timestamp | |
3010 | * @var Date | |
3011 | */ | |
3012 | _date: 0, | |
3013 | ||
8574cbfd AE |
3014 | /** |
3015 | * list of time elements | |
3016 | * @var jQuery | |
3017 | */ | |
3018 | _elements: null, | |
3019 | ||
3020 | /** | |
3021 | * difference between server and local time | |
3022 | * @var integer | |
3023 | */ | |
3024 | _offset: null, | |
3025 | ||
3026 | /** | |
3027 | * current timestamp | |
3028 | * @var integer | |
3029 | */ | |
3030 | _timestamp: 0, | |
3031 | ||
158bd3ca TD |
3032 | /** |
3033 | * Initializes relative datetimes. | |
3034 | */ | |
3035 | init: function() { | |
8574cbfd AE |
3036 | this._elements = $('time.datetime'); |
3037 | this._offset = null; | |
3038 | this._timestamp = 0; | |
158bd3ca TD |
3039 | |
3040 | // calculate relative datetime on init | |
3041 | this._refresh(); | |
3042 | ||
3043 | // re-calculate relative datetime every minute | |
3044 | new WCF.PeriodicalExecuter($.proxy(this._refresh, this), 60000); | |
9f959ced | 3045 | |
9c1e5045 AE |
3046 | // bind dom node inserted listener |
3047 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Date.Time', $.proxy(this._domNodeInserted, this)); | |
3048 | }, | |
9f959ced | 3049 | |
9c1e5045 AE |
3050 | /** |
3051 | * Updates element collection once a DOM node was inserted. | |
3052 | */ | |
3053 | _domNodeInserted: function() { | |
8574cbfd | 3054 | this._elements = $('time.datetime'); |
9c1e5045 | 3055 | this._refresh(); |
158bd3ca TD |
3056 | }, |
3057 | ||
3058 | /** | |
3059 | * Refreshes relative datetime for each element. | |
3060 | */ | |
3061 | _refresh: function() { | |
9cf417b1 MS |
3062 | this._date = new Date(); |
3063 | this._timestamp = (this._date.getTime() - this._date.getMilliseconds()) / 1000; | |
8574cbfd AE |
3064 | if (this._offset === null) { |
3065 | this._offset = this._timestamp - TIME_NOW; | |
3066 | } | |
158bd3ca | 3067 | |
8574cbfd | 3068 | this._elements.each($.proxy(this._refreshElement, this)); |
158bd3ca TD |
3069 | }, |
3070 | ||
3071 | /** | |
3072 | * Refreshes relative datetime for current element. | |
3073 | * | |
3074 | * @param integer index | |
3075 | * @param object element | |
3076 | */ | |
3077 | _refreshElement: function(index, element) { | |
8574cbfd AE |
3078 | var $element = $(element); |
3079 | ||
3080 | if (!$element.attr('title')) { | |
3081 | $element.attr('title', $element.text()); | |
158bd3ca TD |
3082 | } |
3083 | ||
8574cbfd AE |
3084 | var $timestamp = $element.data('timestamp') + this._offset; |
3085 | var $date = $element.data('date'); | |
3086 | var $time = $element.data('time'); | |
3087 | var $offset = $element.data('offset'); | |
158bd3ca | 3088 | |
3ce1cb95 MW |
3089 | // skip for future dates |
3090 | if ($element.data('isFutureDate')) return; | |
3091 | ||
270599e2 MW |
3092 | // timestamp is less than 60 seconds ago |
3093 | if ($timestamp >= this._timestamp || this._timestamp < ($timestamp + 60)) { | |
3094 | $element.text(WCF.Language.get('wcf.date.relative.now')); | |
3095 | } | |
158bd3ca | 3096 | // timestamp is less than 60 minutes ago (display 1 hour ago rather than 60 minutes ago) |
270599e2 | 3097 | else if (this._timestamp < ($timestamp + 3540)) { |
69948aa5 | 3098 | var $minutes = Math.max(Math.round((this._timestamp - $timestamp) / 60), 1); |
fef32fb1 | 3099 | $element.text(WCF.Language.get('wcf.date.relative.minutes', { minutes: $minutes })); |
158bd3ca TD |
3100 | } |
3101 | // timestamp is less than 24 hours ago | |
8574cbfd AE |
3102 | else if (this._timestamp < ($timestamp + 86400)) { |
3103 | var $hours = Math.round((this._timestamp - $timestamp) / 3600); | |
fef32fb1 | 3104 | $element.text(WCF.Language.get('wcf.date.relative.hours', { hours: $hours })); |
158bd3ca | 3105 | } |
54e44c71 AE |
3106 | // timestamp is less than 6 days ago |
3107 | else if (this._timestamp < ($timestamp + 518400)) { | |
9cf417b1 MS |
3108 | var $midnight = new Date(this._date.getFullYear(), this._date.getMonth(), this._date.getDate()); |
3109 | var $days = Math.ceil(($midnight / 1000 - $timestamp) / 86400); | |
9f959ced | 3110 | |
158bd3ca | 3111 | // get day of week |
88ff183f | 3112 | var $dateObj = WCF.Date.Util.getTimezoneDate(($timestamp * 1000), $offset * 1000); |
158bd3ca | 3113 | var $dow = $dateObj.getDay(); |
fef32fb1 | 3114 | var $day = WCF.Language.get('__days')[$dow]; |
158bd3ca | 3115 | |
fef32fb1 | 3116 | $element.text(WCF.Language.get('wcf.date.relative.pastDays', { days: $days, day: $day, time: $time })); |
158bd3ca TD |
3117 | } |
3118 | // timestamp is between ~700 million years BC and last week | |
3119 | else { | |
c02c7d58 | 3120 | var $string = WCF.Language.get('wcf.date.shortDateTimeFormat'); |
8574cbfd | 3121 | $element.text($string.replace(/\%date\%/, $date).replace(/\%time\%/, $time)); |
158bd3ca TD |
3122 | } |
3123 | } | |
81f55d8f | 3124 | }); |
158bd3ca TD |
3125 | |
3126 | /** | |
3127 | * Hash-like dictionary. Based upon idead from Prototype's hash | |
3128 | * | |
3129 | * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/hash.js | |
3130 | */ | |
39e27190 | 3131 | WCF.Dictionary = Class.extend({ |
264e3f79 AE |
3132 | /** |
3133 | * list of variables | |
3134 | * @var object | |
3135 | */ | |
3136 | _variables: { }, | |
3137 | ||
158bd3ca TD |
3138 | /** |
3139 | * Initializes a new dictionary. | |
3140 | */ | |
83428b7f AE |
3141 | init: function() { |
3142 | this._variables = { }; | |
3143 | }, | |
158bd3ca TD |
3144 | |
3145 | /** | |
3146 | * Adds an entry. | |
3147 | * | |
3148 | * @param string key | |
3149 | * @param mixed value | |
3150 | */ | |
3151 | add: function(key, value) { | |
264e3f79 | 3152 | this._variables[key] = value; |
158bd3ca TD |
3153 | }, |
3154 | ||
3155 | /** | |
3156 | * Adds a traditional object to current dataset. | |
3157 | * | |
3158 | * @param object object | |
3159 | */ | |
3160 | addObject: function(object) { | |
3161 | for (var $key in object) { | |
3162 | this.add($key, object[$key]); | |
3163 | } | |
3164 | }, | |
3165 | ||
3166 | /** | |
3167 | * Adds a dictionary to current dataset. | |
3168 | * | |
3169 | * @param object dictionary | |
3170 | */ | |
3171 | addDictionary: function(dictionary) { | |
3172 | dictionary.each($.proxy(function(pair) { | |
3173 | this.add(pair.key, pair.value); | |
3174 | }, this)); | |
3175 | }, | |
3176 | ||
3177 | /** | |
3178 | * Retrieves the value of an entry or returns null if key is not found. | |
3179 | * | |
3180 | * @param string key | |
3181 | * @returns mixed | |
3182 | */ | |
3183 | get: function(key) { | |
3184 | if (this.isset(key)) { | |
264e3f79 | 3185 | return this._variables[key]; |
158bd3ca TD |
3186 | } |
3187 | ||
3188 | return null; | |
3189 | }, | |
3190 | ||
3191 | /** | |
3192 | * Returns true if given key is a valid entry. | |
3193 | * | |
3194 | * @param string key | |
3195 | */ | |
3196 | isset: function(key) { | |
264e3f79 | 3197 | return this._variables.hasOwnProperty(key); |
158bd3ca TD |
3198 | }, |
3199 | ||
3200 | /** | |
3201 | * Removes an entry. | |
3202 | * | |
3203 | * @param string key | |
3204 | */ | |
3205 | remove: function(key) { | |
264e3f79 | 3206 | delete this._variables[key]; |
158bd3ca TD |
3207 | }, |
3208 | ||
3209 | /** | |
3210 | * Iterates through dictionary. | |
3211 | * | |
3212 | * Usage: | |
3213 | * var $hash = new WCF.Dictionary(); | |
3214 | * $hash.add('foo', 'bar'); | |
3215 | * $hash.each(function(pair) { | |
3216 | * // alerts: foo = bar | |
3217 | * alert(pair.key + ' = ' + pair.value); | |
3218 | * }); | |
3219 | * | |
3220 | * @param function callback | |
3221 | */ | |
3222 | each: function(callback) { | |
3223 | if (!$.isFunction(callback)) { | |
3224 | return; | |
3225 | } | |
3226 | ||
264e3f79 AE |
3227 | for (var $key in this._variables) { |
3228 | var $value = this._variables[$key]; | |
158bd3ca TD |
3229 | var $pair = { |
3230 | key: $key, | |
3231 | value: $value | |
3232 | }; | |
3233 | ||
3234 | callback($pair); | |
3235 | } | |
264e3f79 AE |
3236 | }, |
3237 | ||
3238 | /** | |
3239 | * Returns the amount of items. | |
3240 | * | |
3241 | * @return integer | |
3242 | */ | |
3243 | count: function() { | |
3244 | return $.getLength(this._variables); | |
3245 | }, | |
3246 | ||
3247 | /** | |
28410a97 | 3248 | * Returns true if dictionary is empty. |
264e3f79 AE |
3249 | * |
3250 | * @return integer | |
3251 | */ | |
3252 | isEmpty: function() { | |
3253 | return !this.count(); | |
158bd3ca | 3254 | } |
39e27190 | 3255 | }); |
158bd3ca TD |
3256 | |
3257 | /** | |
3258 | * Global language storage. | |
3259 | * | |
3260 | * @see WCF.Dictionary | |
3261 | */ | |
3262 | WCF.Language = { | |
3263 | _variables: new WCF.Dictionary(), | |
3264 | ||
3265 | /** | |
f2e420cc | 3266 | * @see WCF.Dictionary.add() |
158bd3ca TD |
3267 | */ |
3268 | add: function(key, value) { | |
3269 | this._variables.add(key, value); | |
3270 | }, | |
3271 | ||
3272 | /** | |
3273 | * @see WCF.Dictionary.addObject() | |
3274 | */ | |
3275 | addObject: function(object) { | |
3276 | this._variables.addObject(object); | |
3277 | }, | |
3278 | ||
3279 | /** | |
3280 | * Retrieves a variable. | |
3281 | * | |
3282 | * @param string key | |
3283 | * @return mixed | |
3284 | */ | |
12876b18 TD |
3285 | get: function(key, parameters) { |
3286 | // initialize parameters with an empty object | |
66a11454 | 3287 | if (parameters == null) var parameters = { }; |
12876b18 TD |
3288 | |
3289 | var value = this._variables.get(key); | |
3290 | ||
8a236dff TD |
3291 | if (value === null) { |
3292 | // return key again | |
3293 | return key; | |
3294 | } | |
3295 | else if (typeof value === 'string') { | |
12876b18 TD |
3296 | // transform strings into template and try to refetch |
3297 | this.add(key, new WCF.Template(value)); | |
3298 | return this.get(key, parameters); | |
3299 | } | |
66a11454 | 3300 | else if (typeof value.fetch === 'function') { |
12876b18 TD |
3301 | // evaluate templates |
3302 | value = value.fetch(parameters); | |
3303 | } | |
3304 | ||
3305 | return value; | |
158bd3ca TD |
3306 | } |
3307 | }; | |
f2e420cc | 3308 | |
2b4d2743 AE |
3309 | /** |
3310 | * Handles multiple language input fields. | |
3311 | * | |
3312 | * @param string elementID | |
3313 | * @param boolean forceSelection | |
3314 | * @param object values | |
3315 | * @param object availableLanguages | |
3316 | */ | |
7382b20f | 3317 | WCF.MultipleLanguageInput = Class.extend({ |
2b4d2743 AE |
3318 | /** |
3319 | * list of available languages | |
3320 | * @var object | |
3321 | */ | |
3322 | _availableLanguages: {}, | |
9f959ced | 3323 | |
3ef8dee9 AE |
3324 | /** |
3325 | * button element | |
3326 | * @var jQuery | |
3327 | */ | |
3328 | _button: null, | |
3329 | ||
2b4d2743 AE |
3330 | /** |
3331 | * initialization state | |
3332 | * @var boolean | |
3333 | */ | |
3334 | _didInit: false, | |
9f959ced | 3335 | |
2b4d2743 AE |
3336 | /** |
3337 | * target input element | |
3338 | * @var jQuery | |
3339 | */ | |
3340 | _element: null, | |
38047181 AE |
3341 | |
3342 | /** | |
3343 | * true, if data was entered after initialization | |
3344 | * @var boolean | |
3345 | */ | |
3346 | _insertedDataAfterInit: false, | |
9f959ced | 3347 | |
2b4d2743 AE |
3348 | /** |
3349 | * enables multiple language ability | |
3350 | * @var boolean | |
3351 | */ | |
3352 | _isEnabled: false, | |
9f959ced | 3353 | |
2b4d2743 AE |
3354 | /** |
3355 | * enforce multiple language ability | |
3356 | * @var boolean | |
3357 | */ | |
3358 | _forceSelection: false, | |
9f959ced | 3359 | |
2b4d2743 AE |
3360 | /** |
3361 | * currently active language id | |
3362 | * @var integer | |
3363 | */ | |
3364 | _languageID: 0, | |
9f959ced | 3365 | |
2b4d2743 AE |
3366 | /** |
3367 | * language selection list | |
3368 | * @var jQuery | |
3369 | */ | |
3370 | _list: null, | |
9f959ced | 3371 | |
2b4d2743 AE |
3372 | /** |
3373 | * list of language values on init | |
3374 | * @var object | |
3375 | */ | |
3376 | _values: null, | |
9f959ced | 3377 | |
2b4d2743 AE |
3378 | /** |
3379 | * Initializes multiple language ability for given element id. | |
3380 | * | |
3381 | * @param integer elementID | |
3382 | * @param boolean forceSelection | |
3383 | * @param boolean isEnabled | |
3384 | * @param object values | |
3385 | * @param object availableLanguages | |
3386 | */ | |
3387 | init: function(elementID, forceSelection, values, availableLanguages) { | |
3ef8dee9 | 3388 | this._button = null; |
2b4d2743 AE |
3389 | this._element = $('#' + $.wcfEscapeID(elementID)); |
3390 | this._forceSelection = forceSelection; | |
3391 | this._values = values; | |
3392 | this._availableLanguages = availableLanguages; | |
3393 | ||
cc7db7fa AE |
3394 | // unescape values |
3395 | if ($.getLength(this._values)) { | |
3396 | for (var $key in this._values) { | |
3397 | this._values[$key] = WCF.String.unescapeHTML(this._values[$key]); | |
3398 | } | |
3399 | } | |
3400 | ||
2b4d2743 AE |
3401 | // default to current user language |
3402 | this._languageID = LANGUAGE_ID; | |
3403 | if (this._element.length == 0) { | |
3404 | console.debug("[WCF.MultipleLanguageInput] element id '" + elementID + "' is unknown"); | |
3405 | return; | |
3406 | } | |
3407 | ||
3408 | // build selection handler | |
3409 | var $enableOnInit = ($.getLength(this._values) > 0) ? true : false; | |
38047181 | 3410 | this._insertedDataAfterInit = $enableOnInit; |
2b4d2743 AE |
3411 | this._prepareElement($enableOnInit); |
3412 | ||
3413 | // listen for submit event | |
3414 | this._element.parents('form').submit($.proxy(this._submit, this)); | |
9f959ced | 3415 | |
2b4d2743 AE |
3416 | this._didInit = true; |
3417 | }, | |
9f959ced | 3418 | |
2b4d2743 AE |
3419 | /** |
3420 | * Builds language handler. | |
3421 | * | |
3422 | * @param boolean enableOnInit | |
3423 | */ | |
3424 | _prepareElement: function(enableOnInit) { | |
184a8d6d | 3425 | this._element.wrap('<div class="dropdown preInput" />'); |
2b4d2743 | 3426 | var $wrapper = this._element.parent(); |
3ef8dee9 | 3427 | this._button = $('<p class="button dropdownToggle"><span>' + WCF.Language.get('wcf.global.button.disabledI18n') + '</span></p>').prependTo($wrapper); |
184a8d6d | 3428 | |
7382b20f | 3429 | // insert list |
3ef8dee9 | 3430 | this._list = $('<ul class="dropdownMenu"></ul>').insertAfter(this._button); |
7382b20f | 3431 | |
184a8d6d | 3432 | // add a special class if next item is a textarea |
3ef8dee9 AE |
3433 | if (this._button.nextAll('textarea').length) { |
3434 | this._button.addClass('dropdownCaptionTextarea'); | |
184a8d6d AE |
3435 | } |
3436 | else { | |
3ef8dee9 | 3437 | this._button.addClass('dropdownCaption'); |
1b049030 | 3438 | } |
184a8d6d AE |
3439 | |
3440 | // insert available languages | |
3441 | for (var $languageID in this._availableLanguages) { | |
3442 | $('<li><span>' + this._availableLanguages[$languageID] + '</span></li>').data('languageID', $languageID).click($.proxy(this._changeLanguage, this)).appendTo(this._list); | |
3443 | } | |
9f959ced | 3444 | |
184a8d6d AE |
3445 | // disable language input |
3446 | if (!this._forceSelection) { | |
3447 | $('<li class="dropdownDivider" />').appendTo(this._list); | |
3448 | $('<li><span>' + WCF.Language.get('wcf.global.button.disabledI18n') + '</span></li>').click($.proxy(this._disable, this)).appendTo(this._list); | |
3449 | } | |
2b4d2743 | 3450 | |
3ef8dee9 AE |
3451 | WCF.Dropdown.initDropdown(this._button, enableOnInit); |
3452 | ||
46b44739 | 3453 | if (enableOnInit || this._forceSelection) { |
0a7c08e0 AE |
3454 | this._isEnabled = true; |
3455 | ||
2b4d2743 AE |
3456 | // pre-select current language |
3457 | this._list.children('li').each($.proxy(function(index, listItem) { | |
3458 | var $listItem = $(listItem); | |
3459 | if ($listItem.data('languageID') == this._languageID) { | |
3460 | $listItem.trigger('click'); | |
3461 | } | |
3462 | }, this)); | |
3463 | } | |
184a8d6d AE |
3464 | |
3465 | WCF.Dropdown.registerCallback($wrapper.wcfIdentify(), $.proxy(this._handleAction, this)); | |
3466 | }, | |
3467 | ||
3468 | /** | |
3469 | * Handles dropdown actions. | |
3470 | * | |
3ef8dee9 | 3471 | * @param string containerID |
184a8d6d AE |
3472 | * @param string action |
3473 | */ | |
3ef8dee9 AE |
3474 | _handleAction: function(containerID, action) { |
3475 | if (action === 'open') { | |
3476 | this._enable(); | |
3477 | } | |
3478 | else { | |
184a8d6d AE |
3479 | this._closeSelection(); |
3480 | } | |
2b4d2743 | 3481 | }, |
9f959ced | 3482 | |
2b4d2743 AE |
3483 | /** |
3484 | * Enables the language selection or shows the selection if already enabled. | |
3485 | * | |
3486 | * @param object event | |
3487 | */ | |
3488 | _enable: function(event) { | |
3489 | if (!this._isEnabled) { | |
3ef8dee9 | 3490 | var $button = (this._button.is('p')) ? this._button.children('span:eq(0)') : this._button; |
2b4d2743 | 3491 | $button.addClass('active'); |
184a8d6d | 3492 | |
2b4d2743 AE |
3493 | this._isEnabled = true; |
3494 | } | |
184a8d6d | 3495 | |
2b4d2743 AE |
3496 | // toggle list |
3497 | if (this._list.is(':visible')) { | |
2b4d2743 AE |
3498 | this._showSelection(); |
3499 | } | |
2b4d2743 | 3500 | }, |
9f959ced | 3501 | |
2b4d2743 AE |
3502 | /** |
3503 | * Shows the language selection. | |
3504 | */ | |
3505 | _showSelection: function() { | |
3506 | if (this._isEnabled) { | |
3507 | // display status for each language | |
3508 | this._list.children('li').each($.proxy(function(index, listItem) { | |
3509 | var $listItem = $(listItem); | |
3510 | var $languageID = $listItem.data('languageID'); | |
9f959ced | 3511 | |
2b4d2743 AE |
3512 | if ($languageID) { |
3513 | if (this._values[$languageID] && this._values[$languageID] != '') { | |
3514 | $listItem.removeClass('missingValue'); | |
3515 | } | |
3516 | else { | |
3517 | $listItem.addClass('missingValue'); | |
3518 | } | |
3519 | } | |
3520 | }, this)); | |
2b4d2743 AE |
3521 | } |
3522 | }, | |
9f959ced | 3523 | |
2b4d2743 AE |
3524 | /** |
3525 | * Closes the language selection. | |
3526 | */ | |
3527 | _closeSelection: function() { | |
055c009e | 3528 | this._disable(); |
2b4d2743 | 3529 | }, |
9f959ced | 3530 | |
2b4d2743 AE |
3531 | /** |
3532 | * Changes the currently active language. | |
3533 | * | |
3534 | * @param object event | |
3535 | */ | |
3536 | _changeLanguage: function(event) { | |
184a8d6d | 3537 | var $button = $(event.currentTarget); |
38047181 AE |
3538 | this._insertedDataAfterInit = true; |
3539 | ||
2b4d2743 AE |
3540 | // save current value |
3541 | if (this._didInit) { | |
3542 | this._values[this._languageID] = this._element.val(); | |
3543 | } | |
38047181 | 3544 | |
2b4d2743 AE |
3545 | // set new language |
3546 | this._languageID = $button.data('languageID'); | |
3547 | if (this._values[this._languageID]) { | |
3548 | this._element.val(this._values[this._languageID]); | |
3549 | } | |
3550 | else { | |
3551 | this._element.val(''); | |
3552 | } | |
38047181 | 3553 | |
2b4d2743 AE |
3554 | // update marking |
3555 | this._list.children('li').removeClass('active'); | |
3556 | $button.addClass('active'); | |
38047181 | 3557 | |
2b4d2743 | 3558 | // update label |
3ef8dee9 | 3559 | this._button.children('span').addClass('active').text(this._availableLanguages[this._languageID]); |
38047181 | 3560 | |
2b4d2743 | 3561 | // close selection and set focus on input element |
8c368f9a | 3562 | if (this._didInit) { |
8c368f9a MS |
3563 | this._element.blur().focus(); |
3564 | } | |
2b4d2743 | 3565 | }, |
9f959ced | 3566 | |
2b4d2743 AE |
3567 | /** |
3568 | * Disables language selection for current element. | |
0bd3dc4b AE |
3569 | * |
3570 | * @param object event | |
2b4d2743 | 3571 | */ |
0bd3dc4b | 3572 | _disable: function(event) { |
055c009e AE |
3573 | if (event === undefined && this._insertedDataAfterInit) { |
3574 | event = null; | |
3575 | } | |
3576 | ||
3577 | if (this._forceSelection || !this._list || event === null) { | |
46b44739 AE |
3578 | return; |
3579 | } | |
3580 | ||
2b4d2743 | 3581 | // remove active marking |
3ef8dee9 | 3582 | this._button.children('span').removeClass('active').text(WCF.Language.get('wcf.global.button.disabledI18n')); |
9f959ced | 3583 | |
2b4d2743 AE |
3584 | // update element value |
3585 | if (this._values[LANGUAGE_ID]) { | |
3586 | this._element.val(this._values[LANGUAGE_ID]); | |
3587 | } | |
3588 | else { | |
3589 | // no value for current language found, proceed with empty input | |
3590 | this._element.val(); | |
3591 | } | |
84481c65 | 3592 | |
93736c72 MS |
3593 | if (event) { |
3594 | this._list.children('li').removeClass('active'); | |
3595 | $(event.currentTarget).addClass('active'); | |
3596 | } | |
3597 | ||
3a9a460c | 3598 | this._element.blur().focus(); |
055c009e | 3599 | this._insertedDataAfterInit = false; |
2b4d2743 | 3600 | this._isEnabled = false; |
055c009e | 3601 | this._values = { }; |
2b4d2743 | 3602 | }, |
9f959ced | 3603 | |
2b4d2743 AE |
3604 | /** |
3605 | * Prepares language variables on before submit. | |
3606 | */ | |
3607 | _submit: function() { | |
3608 | // insert hidden form elements on before submit | |
3609 | if (!this._isEnabled) { | |
055c009e | 3610 | return 0xDEADBEEF; |
2b4d2743 | 3611 | } |
9f959ced | 3612 | |
2b4d2743 AE |
3613 | // fetch active value |
3614 | if (this._languageID) { | |
3615 | this._values[this._languageID] = this._element.val(); | |
3616 | } | |
9f959ced | 3617 | |
2b4d2743 AE |
3618 | var $form = $(this._element.parents('form')[0]); |
3619 | var $elementID = this._element.wcfIdentify(); | |
9f959ced | 3620 | |
4aae8961 AE |
3621 | for (var $languageID in this._availableLanguages) { |
3622 | if (this._values[$languageID] === undefined) { | |
3623 | this._values[$languageID] = ''; | |
3624 | } | |
3625 | ||
cc7db7fa | 3626 | $('<input type="hidden" name="' + $elementID + '_i18n[' + $languageID + ']" value="' + WCF.String.escapeHTML(this._values[$languageID]) + '" />').appendTo($form); |
2b4d2743 | 3627 | } |
9f959ced | 3628 | |
2b4d2743 AE |
3629 | // remove name attribute to prevent conflict with i18n values |
3630 | this._element.removeAttr('name'); | |
3631 | } | |
7382b20f | 3632 | }); |
2b4d2743 | 3633 | |
52560d9e TD |
3634 | /** |
3635 | * Number utilities. | |
3636 | */ | |
3637 | WCF.Number = { | |
3638 | /** | |
7e969bc7 | 3639 | * Rounds a number to a given number of decimal places. Defaults to 0. |
52560d9e TD |
3640 | * |
3641 | * @param number number | |
7e969bc7 | 3642 | * @param decimalPlaces number of decimal places |
52560d9e TD |
3643 | * @return number |
3644 | */ | |
7e969bc7 TD |
3645 | round: function (number, decimalPlaces) { |
3646 | decimalPlaces = Math.pow(10, (decimalPlaces || 0)); | |
52560d9e | 3647 | |
7e969bc7 | 3648 | return Math.round(number * decimalPlaces) / decimalPlaces; |
52560d9e | 3649 | } |
f4126129 | 3650 | }; |
52560d9e | 3651 | |
158bd3ca TD |
3652 | /** |
3653 | * String utilities. | |
3654 | */ | |
3655 | WCF.String = { | |
52560d9e TD |
3656 | /** |
3657 | * Adds thousands separators to a given number. | |
3658 | * | |
2aebf1f5 | 3659 | * @see http://stackoverflow.com/a/6502556/782822 |
52560d9e TD |
3660 | * @param mixed number |
3661 | * @return string | |
3662 | */ | |
3663 | addThousandsSeparator: function(number) { | |
955082b0 | 3664 | return String(number).replace(/(^-?\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, '$1' + WCF.Language.get('wcf.global.thousandsSeparator')); |
52560d9e TD |
3665 | }, |
3666 | ||
158bd3ca TD |
3667 | /** |
3668 | * Escapes special HTML-characters within a string | |
3669 | * | |
3670 | * @param string string | |
3671 | * @return string | |
3672 | */ | |
3673 | escapeHTML: function (string) { | |
bf3df436 | 3674 | return String(string).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); |
158bd3ca TD |
3675 | }, |
3676 | ||
3677 | /** | |
3678 | * Escapes a String to work with RegExp. | |
7b608580 | 3679 | * |
158bd3ca TD |
3680 | * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25 |
3681 | * @param string string | |
3682 | * @return string | |
3683 | */ | |
3684 | escapeRegExp: function(string) { | |
bf3df436 | 3685 | return String(string).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); |
158bd3ca TD |
3686 | }, |
3687 | ||
3688 | /** | |
52560d9e | 3689 | * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands-separators |
158bd3ca | 3690 | * |
52560d9e | 3691 | * @param mixed number |
158bd3ca TD |
3692 | * @return string |
3693 | */ | |
7e969bc7 TD |
3694 | formatNumeric: function(number, decimalPlaces) { |
3695 | number = String(WCF.Number.round(number, decimalPlaces || 2)); | |
eb9ae99c TD |
3696 | numberParts = number.split('.'); |
3697 | ||
3698 | number = this.addThousandsSeparator(numberParts[0]); | |
3699 | if (numberParts.length > 1) number += WCF.Language.get('wcf.global.decimalPoint') + numberParts[1]; | |
52560d9e | 3700 | |
7e969bc7 TD |
3701 | number = number.replace('-', '\u2212'); |
3702 | ||
3703 | return number; | |
158bd3ca TD |
3704 | }, |
3705 | ||
7a17b105 MS |
3706 | /** |
3707 | * Makes a string's first character lowercase | |
3708 | * | |
3709 | * @param string string | |
3710 | * @return string | |
3711 | */ | |
3712 | lcfirst: function(string) { | |
bf3df436 | 3713 | return String(string).substring(0, 1).toLowerCase() + string.substring(1); |
7a17b105 MS |
3714 | }, |
3715 | ||
158bd3ca | 3716 | /** |
52560d9e | 3717 | * Makes a string's first character uppercase |
158bd3ca | 3718 | * |
52560d9e | 3719 | * @param string string |
158bd3ca TD |
3720 | * @return string |
3721 | */ | |
52560d9e | 3722 | ucfirst: function(string) { |
bf3df436 | 3723 | return String(string).substring(0, 1).toUpperCase() + string.substring(1); |
cc7db7fa AE |
3724 | }, |
3725 | ||
3726 | /** | |
3727 | * Unescapes special HTML-characters within a string | |
3728 | * | |
3729 | * @param string string | |
3730 | * @return string | |
3731 | */ | |
3732 | unescapeHTML: function (string) { | |
3733 | return String(string).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); | |
158bd3ca TD |
3734 | } |
3735 | }; | |
3736 | ||
3737 | /** | |
3738 | * Basic implementation for WCF TabMenus. Use the data attributes 'active' to specify the | |
3739 | * tab which should be shown on init. Furthermore you may specify a 'store' data-attribute | |
3740 | * which will be filled with the currently selected tab. | |
3741 | */ | |
3742 | WCF.TabMenu = { | |
dbd319de AE |
3743 | /** |
3744 | * list of tabmenu containers | |
3745 | * @var object | |
3746 | */ | |
3747 | _containers: { }, | |
3748 | ||
f24f0823 AE |
3749 | /** |
3750 | * initialization state | |
3751 | * @var boolean | |
3752 | */ | |
dbd319de | 3753 | _didInit: false, |
f24f0823 | 3754 | |
158bd3ca TD |
3755 | /** |
3756 | * Initializes all TabMenus | |
3757 | */ | |
3758 | init: function() { | |
0190e4b6 | 3759 | var $containers = $('.tabMenuContainer:not(.staticTabMenuContainer)'); |
dbd319de | 3760 | var self = this; |
f24f0823 AE |
3761 | $containers.each(function(index, tabMenu) { |
3762 | var $tabMenu = $(tabMenu); | |
dbd319de AE |
3763 | var $containerID = $tabMenu.wcfIdentify(); |
3764 | if (self._containers[$containerID]) { | |
3765 | // continue with next container | |
3766 | return true; | |
158bd3ca TD |
3767 | } |
3768 | ||
016198a8 AE |
3769 | if ($tabMenu.data('store') && !$('#' + $tabMenu.data('store')).length) { |
3770 | $('<input type="hidden" name="' + $tabMenu.data('store') + '" value="" id="' + $tabMenu.data('store') + '" />').appendTo($tabMenu.parents('form').find('.formSubmit')); | |
3771 | } | |
3772 | ||
158bd3ca | 3773 | // init jQuery UI TabMenu |
dbd319de | 3774 | self._containers[$containerID] = $tabMenu; |
f24f0823 | 3775 | $tabMenu.wcfTabs({ |
22b63e33 | 3776 | active: false, |
01ec877d AE |
3777 | activate: function(event, eventData) { |
3778 | var $panel = $(eventData.newPanel); | |
184a8d6d | 3779 | var $container = $panel.closest('.tabMenuContainer'); |
158bd3ca TD |
3780 | |
3781 | // store currently selected item | |
016198a8 AE |
3782 | var $tabMenu = $container; |
3783 | while (true) { | |
3784 | // do not trigger on init | |
3785 | if ($tabMenu.data('isParent') === undefined) { | |
3786 | break; | |
3787 | } | |
3788 | ||
3789 | if ($tabMenu.data('isParent')) { | |
3790 | if ($tabMenu.data('store')) { | |
3791 | $('#' + $tabMenu.data('store')).val($panel.attr('id')); | |
3792 | } | |
3793 | ||
3794 | break; | |
3795 | } | |
3796 | else { | |
3797 | $tabMenu = $tabMenu.data('parent'); | |
158bd3ca TD |
3798 | } |
3799 | } | |
f24f0823 AE |
3800 | |
3801 | // set panel id as location hash | |
dbd319de | 3802 | if (WCF.TabMenu._didInit) { |
9ad87a1c AE |
3803 | // do not update history if within an overlay |
3804 | if ($panel.data('inTabMenu') == undefined) { | |
3805 | $panel.data('inTabMenu', ($panel.parents('.dialogContainer').length)); | |
535be52b | 3806 | } |
9ad87a1c AE |
3807 | |
3808 | if (!$panel.data('inTabMenu')) { | |
3809 | if (window.history) { | |
3810 | window.history.pushState(null, document.title, window.location.toString().replace(/#.+$/, '') + '#' + $panel.attr('id')); | |
3811 | } | |
3812 | else { | |
3813 | location.hash = '#' + $panel.attr('id'); | |
3814 | } | |
535be52b | 3815 | } |
f24f0823 | 3816 | } |
158bd3ca TD |
3817 | } |
3818 | }); | |
3819 | ||
47e7e4d9 | 3820 | $tabMenu.data('isParent', ($tabMenu.children('.tabMenuContainer, .tabMenuContent').length > 0)).data('parent', false); |
4b1f0bb6 AE |
3821 | if (!$tabMenu.data('isParent')) { |
3822 | // check if we're a child element | |
3823 | if ($tabMenu.parent().hasClass('tabMenuContainer')) { | |
3824 | $tabMenu.data('parent', $tabMenu.parent()); | |
3825 | } | |
3826 | } | |
158bd3ca | 3827 | }); |
f24f0823 AE |
3828 | |
3829 | // try to resolve location hash | |
dbd319de | 3830 | if (!this._didInit) { |
01ec877d | 3831 | this._selectActiveTab(); |
dbd319de | 3832 | $(window).bind('hashchange', $.proxy(this.selectTabs, this)); |
da804832 | 3833 | |
afa99e58 | 3834 | if (!this._selectErroneousTab()) { |
01ec877d | 3835 | this.selectTabs(); |
da804832 | 3836 | } |
b873148e AE |
3837 | |
3838 | if ($.browser.mozilla && location.hash) { | |
3839 | var $target = $(location.hash); | |
3840 | if ($target.length && $target.hasClass('tabMenuContent')) { | |
3841 | var $offset = $target.offset(); | |
3842 | window.scrollTo($offset.left, $offset.top); | |
3843 | } | |
3844 | } | |
dbd319de AE |
3845 | } |
3846 | ||
da804832 AE |
3847 | this._didInit = true; |
3848 | }, | |
3849 | ||
1eda8a97 MS |
3850 | /** |
3851 | * Reloads the tab menus. | |
3852 | */ | |
3853 | reload: function() { | |
3854 | this._containers = { }; | |
3855 | this.init(); | |
3856 | }, | |
3857 | ||
da804832 | 3858 | /** |
28410a97 MS |
3859 | * Force display of first erroneous tab and returns true if at least one |
3860 | * tab contains an error. | |
da804832 AE |
3861 | * |
3862 | * @return boolean | |
3863 | */ | |
3864 | _selectErroneousTab: function() { | |
e011af9e | 3865 | var $foundErrors = false; |
4b1f0bb6 AE |
3866 | for (var $containerID in this._containers) { |
3867 | var $tabMenu = this._containers[$containerID]; | |
da804832 | 3868 | |
e011af9e AE |
3869 | if ($tabMenu.find('.formError').length) { |
3870 | $foundErrors = true; | |
3871 | ||
3872 | if (!$tabMenu.data('isParent')) { | |
3873 | while (true) { | |
3874 | if ($tabMenu.data('parent') === false) { | |
3875 | break; | |
3876 | } | |
3877 | ||
3878 | $tabMenu = $tabMenu.data('parent').wcfTabs('selectTab', $tabMenu.wcfIdentify()); | |
4b1f0bb6 AE |
3879 | } |
3880 | ||
e011af9e | 3881 | return true; |
4b1f0bb6 | 3882 | } |
e011af9e AE |
3883 | } |
3884 | } | |
3885 | ||
3886 | // found an error in a non-nested tab menu | |
3887 | if ($foundErrors) { | |
3888 | for (var $containerID in this._containers) { | |
3889 | var $tabMenu = this._containers[$containerID]; | |
3890 | var $formError = $tabMenu.find('.formError:eq(0)'); | |
4b1f0bb6 | 3891 | |
e011af9e AE |
3892 | if ($formError.length) { |
3893 | // find the tab container | |
3894 | $tabMenu.wcfTabs('selectTab', $formError.parents('.tabMenuContent').wcfIdentify()); | |
3895 | ||
3896 | while (true) { | |
3897 | if ($tabMenu.data('parent') === false) { | |
3898 | break; | |
3899 | } | |
3900 | ||
3901 | $tabMenu = $tabMenu.data('parent').wcfTabs('selectTab', $tabMenu.wcfIdentify()); | |
3902 | } | |
3903 | ||
3904 | return true; | |
3905 | } | |
4b1f0bb6 AE |
3906 | } |
3907 | } | |
3908 | ||
da804832 AE |
3909 | return false; |
3910 | }, | |
3911 | ||
3912 | /** | |
3913 | * Selects the active tab menu item. | |
3914 | */ | |
3915 | _selectActiveTab: function() { | |
3916 | for (var $containerID in this._containers) { | |
3917 | var $tabMenu = this._containers[$containerID]; | |
3918 | if ($tabMenu.data('active')) { | |
3919 | var $index = $tabMenu.data('active'); | |
3920 | var $subIndex = null; | |
3921 | if (/-/.test($index)) { | |
3922 | var $tmp = $index.split('-'); | |
3923 | $index = $tmp[0]; | |
3924 | $subIndex = $tmp[1]; | |
3925 | } | |
3926 | ||
3927 | $tabMenu.find('.tabMenuContent').each(function(innerIndex, tabMenuItem) { | |
3928 | var $tabMenuItem = $(tabMenuItem); | |
3929 | if ($tabMenuItem.wcfIdentify() == $index) { | |
3930 | $tabMenu.wcfTabs('select', innerIndex); | |
da804832 AE |
3931 | if ($subIndex !== null) { |
3932 | if ($tabMenuItem.hasClass('tabMenuContainer')) { | |
22b63e33 | 3933 | $tabMenuItem.wcfTabs('selectTab', $tabMenu.data('active')); |
da804832 AE |
3934 | } |
3935 | else { | |
22b63e33 | 3936 | $tabMenu.wcfTabs('selectTab', $tabMenu.data('active')); |
da804832 AE |
3937 | } |
3938 | } | |
3939 | ||
3940 | return false; | |
3941 | } | |
3942 | }); | |
3943 | } | |
3944 | } | |
dbd319de AE |
3945 | }, |
3946 | ||
3947 | /** | |
3948 | * Resolves location hash to display tab menus. | |
d0fac0bd AE |
3949 | * |
3950 | * @return boolean | |
dbd319de AE |
3951 | */ |
3952 | selectTabs: function() { | |
3953 | if (location.hash) { | |
3954 | var $hash = location.hash.substr(1); | |
dbd319de | 3955 | |
d0fac0bd AE |
3956 | // try to find matching tab menu container |
3957 | var $tabMenu = $('#' + $.wcfEscapeID($hash)); | |
3958 | if ($tabMenu.length === 1 && $tabMenu.hasClass('ui-tabs-panel')) { | |
3959 | $tabMenu = $tabMenu.parent('.ui-tabs'); | |
3960 | if ($tabMenu.length) { | |
22b63e33 | 3961 | $tabMenu.wcfTabs('selectTab', $hash); |
d0fac0bd AE |
3962 | |
3963 | // check if this is a nested tab menu | |
3964 | if ($tabMenu.hasClass('ui-tabs-panel')) { | |
3965 | $hash = $tabMenu.wcfIdentify(); | |
3966 | $tabMenu = $tabMenu.parent('.ui-tabs'); | |
3967 | if ($tabMenu.length) { | |
22b63e33 | 3968 | $tabMenu.wcfTabs('selectTab', $hash); |
f24f0823 | 3969 | } |
f24f0823 | 3970 | } |
dbd319de | 3971 | |
d0fac0bd | 3972 | return true; |
dbd319de | 3973 | } |
f24f0823 AE |
3974 | } |
3975 | } | |
d0fac0bd AE |
3976 | |
3977 | return false; | |
158bd3ca TD |
3978 | } |
3979 | }; | |
3980 | ||
3981 | /** | |
9f959ced MS |
3982 | * Templates that may be fetched more than once with different variables. |
3983 | * Based upon ideas from Prototype's template. | |
158bd3ca TD |
3984 | * |
3985 | * Usage: | |
3986 | * var myTemplate = new WCF.Template('{$hello} World'); | |
3987 | * myTemplate.fetch({ hello: 'Hi' }); // Hi World | |
3988 | * myTemplate.fetch({ hello: 'Hello' }); // Hello World | |
3989 | * | |
3990 | * my2ndTemplate = new WCF.Template('{@$html}{$html}'); | |
3991 | * my2ndTemplate.fetch({ html: '<b>Test</b>' }); // <b>Test</b><b>Test</b> | |
9f959ced | 3992 | * |
158bd3ca TD |
3993 | * var my3rdTemplate = new WCF.Template('You can use {literal}{$variable}{/literal}-Tags here'); |
3994 | * my3rdTemplate.fetch({ variable: 'Not shown' }); // You can use {$variable}-Tags here | |
3995 | * | |
158bd3ca TD |
3996 | * @param template template-content |
3997 | * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/template.js | |
3998 | */ | |
39e27190 | 3999 | WCF.Template = Class.extend({ |
158bd3ca TD |
4000 | /** |
4001 | * Prepares template | |
4002 | * | |
4003 | * @param $template template-content | |
4004 | */ | |
955082b0 | 4005 | init: function(template) { |
35a7384e | 4006 | var $literals = new WCF.Dictionary(); |
66a11454 | 4007 | var $tagID = 0; |
35a7384e | 4008 | |
ec591c4f TD |
4009 | // escape \ and ' and newlines |
4010 | template = template.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/(\r\n|\n|\r)/g, '\\n'); | |
158bd3ca TD |
4011 | |
4012 | // save literal-tags | |
ec591c4f | 4013 | template = template.replace(/\{literal\}(.*?)\{\/literal\}/g, $.proxy(function(match) { |
158bd3ca TD |
4014 | // hopefully no one uses this string in one of his templates |
4015 | var id = '@@@@@@@@@@@'+Math.random()+'@@@@@@@@@@@'; | |
35a7384e | 4016 | $literals.add(id, match.replace(/\{\/?literal\}/g, '')); |
158bd3ca TD |
4017 | |
4018 | return id; | |
4019 | }, this)); | |
158bd3ca | 4020 | |
16872914 TD |
4021 | // remove comments |
4022 | template = template.replace(/\{\*.*?\*\}/g, ''); | |
4023 | ||
ec591c4f | 4024 | var parseParameterList = function(parameterString) { |
6c9ce157 TD |
4025 | var $chars = parameterString.split(''); |
4026 | var $parameters = { }; | |
4027 | var $inName = true; | |
4028 | var $name = ''; | |
4029 | var $value = ''; | |
a446eaa4 TD |
4030 | var $doubleQuoted = false; |
4031 | var $singleQuoted = false; | |
6c9ce157 | 4032 | var $escaped = false; |
ec591c4f | 4033 | |
6c9ce157 TD |
4034 | for (var $i = 0, $max = $chars.length; $i < $max; $i++) { |
4035 | var $char = $chars[$i]; | |
89be55b6 | 4036 | if ($inName && $char != '=' && $char != ' ') $name += $char; |
6c9ce157 TD |
4037 | else if ($inName && $char == '=') { |
4038 | $inName = false; | |
a446eaa4 TD |
4039 | $singleQuoted = false; |
4040 | $doubleQuoted = false; | |
6c9ce157 TD |
4041 | $escaped = false; |
4042 | } | |
a446eaa4 | 4043 | else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == ' ') { |
6c9ce157 TD |
4044 | $inName = true; |
4045 | $parameters[$name] = $value; | |
4046 | $value = $name = ''; | |
4047 | } | |
a446eaa4 TD |
4048 | else if (!$inName && $singleQuoted && !$escaped && $char == "'") { |
4049 | $singleQuoted = false; | |
6c9ce157 TD |
4050 | $value += $char; |
4051 | } | |
a446eaa4 TD |
4052 | else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == "'") { |
4053 | $singleQuoted = true; | |
6c9ce157 TD |
4054 | $value += $char; |
4055 | } | |
a446eaa4 TD |
4056 | else if (!$inName && $doubleQuoted && !$escaped && $char == '"') { |
4057 | $doubleQuoted = false; | |
4058 | $value += $char; | |
4059 | } | |
4060 | else if (!$inName && !$singleQuoted && !$doubleQuoted && $char == '"') { | |
4061 | $doubleQuoted = true; | |
4062 | $value += $char; | |
4063 | } | |
4064 | else if (!$inName && ($doubleQuoted || $singleQuoted) && !$escaped && $char == '\\') { | |
6c9ce157 TD |
4065 | $escaped = true; |
4066 | $value += $char; | |
4067 | } | |
4068 | else if (!$inName) { | |
4069 | $escaped = false; | |
4070 | $value += $char; | |
4071 | } | |
4072 | } | |
4073 | $parameters[$name] = $value; | |
4074 | ||
a446eaa4 | 4075 | if ($doubleQuoted || $singleQuoted || $escaped) throw new Error('Syntax error in parameterList: "' + parameterString + '"'); |
6c9ce157 TD |
4076 | |
4077 | return $parameters; | |
4078 | }; | |
4079 | ||
ec591c4f TD |
4080 | var unescape = function(string) { |
4081 | return string.replace(/\\n/g, "\n").replace(/\\\\/g, '\\').replace(/\\'/g, "'"); | |
4082 | }; | |
4083 | ||
4a107b66 | 4084 | template = template.replace(/\{(\$[^\}]+?)\}/g, function(_, content) { |
5a6819fe | 4085 | content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])")); |
955082b0 | 4086 | |
35a7384e | 4087 | return "' + WCF.String.escapeHTML(" + content + ") + '"; |
ec591c4f TD |
4088 | }) |
4089 | // Numeric Variable | |
4a107b66 | 4090 | .replace(/\{#(\$[^\}]+?)\}/g, function(_, content) { |
5a6819fe | 4091 | content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])")); |
955082b0 | 4092 | |
35a7384e | 4093 | return "' + WCF.String.formatNumeric(" + content + ") + '"; |
ec591c4f TD |
4094 | }) |
4095 | // Variable without escaping | |
4a107b66 | 4096 | .replace(/\{@(\$[^\}]+?)\}/g, function(_, content) { |
5a6819fe | 4097 | content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])")); |
955082b0 | 4098 | |
35a7384e | 4099 | return "' + " + content + " + '"; |
35a7384e | 4100 | }) |
4650d4be | 4101 | // {lang}foo{/lang} |
7b608580 | 4102 | .replace(/{lang}(.+?){\/lang}/g, function(_, content) { |
4650d4be MS |
4103 | return "' + WCF.Language.get('" + unescape(content) + "') + '"; |
4104 | }) | |
ec591c4f TD |
4105 | // {if} |
4106 | .replace(/\{if (.+?)\}/g, function(_, content) { | |
5a6819fe | 4107 | content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])")); |
0f5aa615 | 4108 | |
ec591c4f TD |
4109 | return "';\n" + |
4110 | "if (" + content + ") {\n" + | |
4111 | " $output += '"; | |
35a7384e | 4112 | }) |
ec591c4f TD |
4113 | // {elseif} |
4114 | .replace(/\{else ?if (.+?)\}/g, function(_, content) { | |
5a6819fe | 4115 | content = unescape(content.replace(/\$([^.\[\s]+)/g, "(v['$1'])")); |
ec591c4f TD |
4116 | |
4117 | return "';\n" + | |
4118 | "}\n" + | |
4119 | "else if (" + content + ") {\n" + | |
4120 | " $output += '"; | |
4121 | }) | |
4122 | // {implode} | |
4123 | .replace(/\{implode (.+?)\}/g, function(_, content) { | |
66a11454 | 4124 | $tagID++; |
6c9ce157 TD |
4125 | |
4126 | content = content.replace(/\\\\/g, '\\').replace(/\\'/g, "'"); | |
4127 | var $parameters = parseParameterList(content); | |
4128 | ||
4129 | if (typeof $parameters['from'] === 'undefined') throw new Error('Missing from attribute in implode-tag'); | |
4130 | if (typeof $parameters['item'] === 'undefined') throw new Error('Missing item attribute in implode-tag'); | |
4131 | if (typeof $parameters['glue'] === 'undefined') $parameters['glue'] = "', '"; | |
ec591c4f | 4132 | |
5a6819fe | 4133 | $parameters['from'] = $parameters['from'].replace(/\$([^.\[\s]+)/g, "(v.$1)"); |
6c9ce157 | 4134 | |
ec591c4f TD |
4135 | return "';\n"+ |
4136 | "var $implode_" + $tagID + " = false;\n" + | |
4137 | "for ($implodeKey_" + $tagID + " in " + $parameters['from'] + ") {\n" + | |
4138 | " v[" + $parameters['item'] + "] = " + $parameters['from'] + "[$implodeKey_" + $tagID + "];\n" + | |
4139 | (typeof $parameters['key'] !== 'undefined' ? " v[" + $parameters['key'] + "] = $implodeKey_" + $tagID + ";\n" : "") + | |
4140 | " if ($implode_" + $tagID + ") $output += " + $parameters['glue'] + ";\n" + | |
4141 | " $implode_" + $tagID + " = true;\n" + | |
4142 | " $output += '"; | |
66a11454 | 4143 | }) |
ec591c4f TD |
4144 | // {foreach} |
4145 | .replace(/\{foreach (.+?)\}/g, function(_, content) { | |
66a11454 TD |
4146 | $tagID++; |
4147 | ||
4148 | content = content.replace(/\\\\/g, '\\').replace(/\\'/g, "'"); | |
4149 | var $parameters = parseParameterList(content); | |
4150 | ||
4151 | if (typeof $parameters['from'] === 'undefined') throw new Error('Missing from attribute in foreach-tag'); | |
4152 | if (typeof $parameters['item'] === 'undefined') throw new Error('Missing item attribute in foreach-tag'); | |
5a6819fe | 4153 | $parameters['from'] = $parameters['from'].replace(/\$([^.\[\s]+)/g, "(v.$1)"); |
66a11454 | 4154 | |
ec591c4f TD |
4155 | return "';\n" + |
4156 | "$foreach_"+$tagID+" = false;\n" + | |
4157 | "for ($foreachKey_" + $tagID + " in " + $parameters['from'] + ") {\n" + | |
4158 | " $foreach_"+$tagID+" = true;\n" + | |
4159 | " break;\n" + | |
4160 | "}\n" + | |
4161 | "if ($foreach_"+$tagID+") {\n" + | |
4162 | " for ($foreachKey_" + $tagID + " in " + $parameters['from'] + ") {\n" + | |
4163 | " v[" + $parameters['item'] + "] = " + $parameters['from'] + "[$foreachKey_" + $tagID + "];\n" + | |
4164 | (typeof $parameters['key'] !== 'undefined' ? " v[" + $parameters['key'] + "] = $foreachKey_" + $tagID + ";\n" : "") + | |
4165 | " $output += '"; | |
6c9ce157 | 4166 | }) |
ec591c4f TD |
4167 | // {foreachelse} |
4168 | .replace(/\{foreachelse\}/g, | |
4169 | "';\n" + | |
4170 | " }\n" + | |
4171 | "}\n" + | |
4172 | "else {\n" + | |
4173 | " {\n" + | |
4174 | " $output += '" | |
4175 | ) | |
4176 | // {/foreach} | |
4177 | .replace(/\{\/foreach\}/g, | |
4178 | "';\n" + | |
4179 | " }\n" + | |
4180 | "}\n" + | |
4181 | "$output += '" | |
4182 | ) | |
4183 | // {else} | |
4184 | .replace(/\{else\}/g, | |
4185 | "';\n" + | |
4186 | "}\n" + | |
4187 | "else {\n" + | |
4188 | " $output += '" | |
4189 | ) | |
4190 | // {/if} and {/implode} | |
4191 | .replace(/\{\/(if|implode)\}/g, | |
4192 | "';\n" + | |
4193 | "}\n" + | |
4194 | "$output += '" | |
4195 | ); | |
4196 | ||
4197 | // call callback | |
71b79d8a TD |
4198 | for (var key in WCF.Template.callbacks) { |
4199 | template = WCF.Template.callbacks[key](template); | |
158bd3ca TD |
4200 | } |
4201 | ||
4202 | // insert delimiter tags | |
35a7384e | 4203 | template = template.replace('{ldelim}', '{').replace('{rdelim}', '}'); |
158bd3ca | 4204 | |
ec591c4f | 4205 | $literals.each(function(pair) { |
35a7384e | 4206 | template = template.replace(pair.key, pair.value); |
158bd3ca TD |
4207 | }); |
4208 | ||
35a7384e | 4209 | template = "$output += '" + template + "';"; |
4a107b66 | 4210 | |
a2067610 | 4211 | try { |
e338ca2c | 4212 | this.fetch = new Function("v", "if (typeof v != 'object') { v = {}; } v.__window = window; v.__wcf = window.WCF; var $output = ''; " + template + ' return $output;'); |
a2067610 TD |
4213 | } |
4214 | catch (e) { | |
4215 | console.debug("var $output = ''; " + template + ' return $output;'); | |
4216 | throw e; | |
4217 | } | |
158bd3ca TD |
4218 | }, |
4219 | ||
4220 | /** | |
35a7384e | 4221 | * Fetches the template with the given variables. |
7b608580 | 4222 | * |
955082b0 TD |
4223 | * @param v variables to insert |
4224 | * @return parsed template | |
158bd3ca | 4225 | */ |
520d73f1 | 4226 | fetch: function(v) { |
35a7384e | 4227 | // this will be replaced in the init function |
158bd3ca | 4228 | } |
39e27190 | 4229 | }); |
158bd3ca TD |
4230 | |
4231 | /** | |
71b79d8a | 4232 | * Array of callbacks that will be called after parsing the included tags. Only applies to Templates compiled after the callback was added. |
158bd3ca | 4233 | * |
71b79d8a | 4234 | * @var array<Function> |
158bd3ca | 4235 | */ |
71b79d8a | 4236 | WCF.Template.callbacks = [ ]; |
158bd3ca TD |
4237 | |
4238 | /** | |
4239 | * Toggles options. | |
4240 | * | |
4241 | * @param string element | |
4242 | * @param array showItems | |
4243 | * @param array hideItems | |
14c5ff9e | 4244 | * @param function callback |
158bd3ca | 4245 | */ |
39e27190 | 4246 | WCF.ToggleOptions = Class.extend({ |
158bd3ca TD |
4247 | /** |
4248 | * target item | |
4249 | * | |
4250 | * @var jQuery | |
4251 | */ | |
4252 | _element: null, | |
4253 | ||
4254 | /** | |
4255 | * list of items to be shown | |
4256 | * | |
4257 | * @var array | |
4258 | */ | |
4259 | _showItems: [], | |
4260 | ||
4261 | /** | |
4262 | * list of items to be hidden | |
4263 | * | |
4264 | * @var array | |
4265 | */ | |
4266 | _hideItems: [], | |
14c5ff9e DR |
4267 | |
4268 | /** | |
4269 | * callback after options were toggled | |
4270 | * | |
4271 | * @var function | |
4272 | */ | |
4273 | _callback: null, | |
158bd3ca TD |
4274 | |
4275 | /** | |
4276 | * Initializes option toggle. | |
4277 | * | |
4278 | * @param string element | |
4279 | * @param array showItems | |
4280 | * @param array hideItems | |
14c5ff9e | 4281 | * @param function callback |
158bd3ca | 4282 | */ |
14c5ff9e | 4283 | init: function(element, showItems, hideItems, callback) { |
158bd3ca TD |
4284 | this._element = $('#' + element); |
4285 | this._showItems = showItems; | |
4286 | this._hideItems = hideItems; | |
14c5ff9e DR |
4287 | if (callback !== undefined) { |
4288 | this._callback = callback; | |
4289 | } | |
158bd3ca TD |
4290 | |
4291 | // bind event | |
4292 | this._element.click($.proxy(this._toggle, this)); | |
4293 | ||
4294 | // execute toggle on init | |
4295 | this._toggle(); | |
4296 | }, | |
4297 | ||
4298 | /** | |
4299 | * Toggles items. | |
4300 | */ | |
4301 | _toggle: function() { | |
1a50aae0 | 4302 | if (!this._element.prop('checked')) return; |
158bd3ca TD |
4303 | |
4304 | for (var $i = 0, $length = this._showItems.length; $i < $length; $i++) { | |
4305 | var $item = this._showItems[$i]; | |
4306 | ||
4307 | $('#' + $item).show(); | |
4308 | } | |
4309 | ||
4310 | for (var $i = 0, $length = this._hideItems.length; $i < $length; $i++) { | |
4311 | var $item = this._hideItems[$i]; | |
4312 | ||
4313 | $('#' + $item).hide(); | |
4314 | } | |
14c5ff9e DR |
4315 | |
4316 | if (this._callback !== null) { | |
e42f6447 | 4317 | this._callback(); |
14c5ff9e | 4318 | } |
158bd3ca | 4319 | } |
39e27190 | 4320 | }); |
158bd3ca | 4321 | |
1c746fe3 AE |
4322 | /** |
4323 | * Namespace for all kind of collapsible containers. | |
4324 | */ | |
4325 | WCF.Collapsible = {}; | |
4326 | ||
4327 | /** | |
4328 | * Simple implementation for collapsible content, neither does it | |
4329 | * store its state nor does it allow AJAX callbacks to fetch content. | |
4330 | */ | |
4331 | WCF.Collapsible.Simple = { | |
4332 | /** | |
4333 | * Initializes collapsibles. | |
4334 | */ | |
4335 | init: function() { | |
b29fbb53 | 4336 | $('.jsCollapsible').each($.proxy(function(index, button) { |
1c746fe3 AE |
4337 | this._initButton(button); |
4338 | }, this)); | |
4339 | }, | |
4340 | ||
4341 | /** | |
4342 | * Binds an event listener on all buttons triggering the collapsible. | |
4343 | * | |
4344 | * @param object button | |
4345 | */ | |
4346 | _initButton: function(button) { | |
4347 | var $button = $(button); | |
4348 | var $isOpen = $button.data('isOpen'); | |
4349 | ||
4350 | if (!$isOpen) { | |
4351 | // hide container on init | |
ec161ab2 | 4352 | $('#' + $button.data('collapsibleContainer')).hide(); |
1c746fe3 AE |
4353 | } |
4354 | ||
4355 | $button.click($.proxy(this._toggle, this)); | |
4356 | }, | |
4357 | ||
4358 | /** | |
4359 | * Toggles collapsible containers on click. | |
4360 | * | |
4361 | * @param object event | |
4362 | */ | |
4363 | _toggle: function(event) { | |
ec161ab2 | 4364 | var $button = $(event.currentTarget); |
1c746fe3 AE |
4365 | var $isOpen = $button.data('isOpen'); |
4366 | var $target = $('#' + $.wcfEscapeID($button.data('collapsibleContainer'))); | |
4367 | ||
4368 | if ($isOpen) { | |
4369 | $target.stop().wcfBlindOut('vertical', $.proxy(function() { | |
556973c1 | 4370 | this._toggleImage($button); |
1c746fe3 AE |
4371 | }, this)); |
4372 | $isOpen = false; | |
4373 | } | |
4374 | else { | |
4375 | $target.stop().wcfBlindIn('vertical', $.proxy(function() { | |
556973c1 | 4376 | this._toggleImage($button); |
1c746fe3 AE |
4377 | }, this)); |
4378 | $isOpen = true; | |
4379 | } | |
4380 | ||
4381 | $button.data('isOpen', $isOpen); | |
4382 | ||
4383 | // suppress event | |
4384 | event.stopPropagation(); | |
4385 | return false; | |
4386 | }, | |
4387 | ||
4388 | /** | |
4389 | * Toggles image of target button. | |
4390 | * | |
4391 | * @param jQuery button | |
1c746fe3 | 4392 | */ |
556973c1 MW |
4393 | _toggleImage: function(button) { |
4394 | var $icon = button.find('span.icon'); | |
4395 | if (button.data('isOpen')) { | |
4396 | $icon.removeClass('icon-chevron-right').addClass('icon-chevron-down'); | |
4397 | } | |
4398 | else { | |
4399 | $icon.removeClass('icon-chevron-down').addClass('icon-chevron-right'); | |
1c746fe3 | 4400 | } |
1c746fe3 AE |
4401 | } |
4402 | }; | |
4403 | ||
878d0d80 AE |
4404 | /** |
4405 | * Basic implementation for collapsible containers with AJAX support. Results for open | |
4406 | * and closed state will be cached. | |
9f959ced | 4407 | * |
878d0d80 AE |
4408 | * @param string className |
4409 | */ | |
4410 | WCF.Collapsible.Remote = Class.extend({ | |
4411 | /** | |
4412 | * class name | |
4413 | * @var string | |
4414 | */ | |
4415 | _className: '', | |
9f959ced | 4416 | |
878d0d80 AE |
4417 | /** |
4418 | * list of active containers | |
4419 | * @var object | |
4420 | */ | |
4421 | _containers: {}, | |
9f959ced | 4422 | |
878d0d80 AE |
4423 | /** |
4424 | * container meta data | |
4425 | * @var object | |
4426 | */ | |
4427 | _containerData: {}, | |
9f959ced | 4428 | |
878d0d80 AE |
4429 | /** |
4430 | * action proxy | |
4431 | * @var WCF.Action.Proxy | |
4432 | */ | |
4433 | _proxy: null, | |
9f959ced | 4434 | |
878d0d80 AE |
4435 | /** |
4436 | * Initializes the controller for collapsible containers with AJAX support. | |
4437 | * | |
4438 | * @param string className | |
4439 | */ | |
4440 | init: function(className) { | |
4441 | this._className = className; | |
878d0d80 AE |
4442 | this._proxy = new WCF.Action.Proxy({ |
4443 | success: $.proxy(this._success, this) | |
4444 | }); | |
4445 | ||
4446 | // initialize each container | |
0959ca1d AE |
4447 | this._init(); |
4448 | ||
4449 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Collapsible.Remote', $.proxy(this._init, this)); | |
4450 | }, | |
4451 | ||
4452 | /** | |
4453 | * Initializes a collapsible container. | |
4454 | * | |
4455 | * @param string containerID | |
4456 | */ | |
4457 | _init: function(containerID) { | |
4458 | this._getContainers().each($.proxy(function(index, container) { | |
878d0d80 | 4459 | var $container = $(container); |
c4b3ae32 | 4460 | var $containerID = $container.wcfIdentify(); |
878d0d80 | 4461 | |
0959ca1d AE |
4462 | if (this._containers[$containerID] === undefined) { |
4463 | this._containers[$containerID] = $container; | |
4464 | ||
4465 | this._initContainer($containerID); | |
4466 | } | |
878d0d80 AE |
4467 | }, this)); |
4468 | }, | |
4469 | ||
c4b3ae32 AE |
4470 | /** |
4471 | * Initializes a collapsible container. | |
4472 | * | |
4473 | * @param string containerID | |
4474 | */ | |
8805f7bf | 4475 | _initContainer: function(containerID) { |
77c43423 MW |
4476 | var $target = this._getTarget(containerID); |
4477 | var $buttonContainer = this._getButtonContainer(containerID); | |
4478 | var $button = this._createButton(containerID, $buttonContainer); | |
4479 | ||
4480 | // store container meta data | |
4481 | this._containerData[containerID] = { | |
4482 | button: $button, | |
4483 | buttonContainer: $buttonContainer, | |
c4b3ae32 | 4484 | isOpen: this._containers[containerID].data('isOpen'), |
77c43423 MW |
4485 | target: $target |
4486 | }; | |
2851eadd MS |
4487 | |
4488 | // add 'jsCollapsed' CSS class | |
4489 | if (!this._containers[containerID].data('isOpen')) { | |
4490 | $('#' + containerID).addClass('jsCollapsed'); | |
4491 | } | |
77c43423 MW |
4492 | }, |
4493 | ||
878d0d80 AE |
4494 | /** |
4495 | * Returns a collection of collapsible containers. | |
4496 | * | |
4497 | * @return jQuery | |
4498 | */ | |
4499 | _getContainers: function() { }, | |
4500 | ||
4501 | /** | |
4502 | * Returns the target element for current collapsible container. | |
4503 | * | |
4504 | * @param integer containerID | |
4505 | * @return jQuery | |
4506 | */ | |
4507 | _getTarget: function(containerID) { }, | |
4508 | ||
4509 | /** | |
4510 | * Returns the button container for current collapsible container. | |
4511 | * | |
4512 | * @param integer containerID | |
4513 | * @return jQuery | |
4514 | */ | |
4515 | _getButtonContainer: function(containerID) { }, | |
4516 | ||
4517 | /** | |
4518 | * Creates the toggle button. | |
4519 | * | |
4520 | * @param integer containerID | |
4521 | * @param jQuery buttonContainer | |
4522 | */ | |
4523 | _createButton: function(containerID, buttonContainer) { | |
4524 | var $isOpen = this._containers[containerID].data('isOpen'); | |
556973c1 | 4525 | var $button = $('<span class="collapsibleButton jsTooltip pointer icon icon16 icon-' + ($isOpen ? 'chevron-down' : 'chevron-right') + '" title="'+WCF.Language.get('wcf.global.button.collapsible')+'">').prependTo(buttonContainer); |
878d0d80 | 4526 | $button.data('containerID', containerID).click($.proxy(this._toggleContainer, this)); |
9f959ced | 4527 | |
03812bbc | 4528 | return $button; |
878d0d80 AE |
4529 | }, |
4530 | ||
4531 | /** | |
4532 | * Toggles a container. | |
4533 | * | |
4534 | * @param object event | |
4535 | */ | |
4536 | _toggleContainer: function(event) { | |
b8a3ccb7 | 4537 | var $button = $(event.currentTarget); |
878d0d80 | 4538 | var $containerID = $button.data('containerID'); |
f4126129 | 4539 | var $isOpen = this._containerData[$containerID].isOpen; |
878d0d80 AE |
4540 | var $state = ($isOpen) ? 'open' : 'close'; |
4541 | var $newState = ($isOpen) ? 'close' : 'open'; | |
4542 | ||
878d0d80 AE |
4543 | // fetch content state via AJAX |
4544 | this._proxy.setOption('data', { | |
c4b3ae32 | 4545 | actionName: 'loadContainer', |
878d0d80 | 4546 | className: this._className, |
c05528d8 | 4547 | interfaceName: 'wcf\\data\\ILoadableContainerAction', |
c4b3ae32 | 4548 | objectIDs: [ this._getObjectID($containerID) ], |
88a85f47 | 4549 | parameters: $.extend(true, { |
878d0d80 AE |
4550 | containerID: $containerID, |
4551 | currentState: $state, | |
a083bb38 | 4552 | newState: $newState |
88a85f47 | 4553 | }, this._getAdditionalParameters($containerID)) |
878d0d80 AE |
4554 | }); |
4555 | this._proxy.sendRequest(); | |
9f959ced | 4556 | |
2851eadd MS |
4557 | // toogle 'jsCollapsed' CSS class |
4558 | $('#' + $containerID).toggleClass('jsCollapsed'); | |
4559 | ||
03812bbc | 4560 | // set spinner for current button |
556973c1 | 4561 | // this._exchangeIcon($button); |
03812bbc | 4562 | }, |
c4b3ae32 AE |
4563 | |
4564 | /** | |
4565 | * Exchanges button icon. | |
4566 | * | |
4567 | * @param jQuery button | |
4568 | * @param string newIcon | |
4569 | */ | |
4570 | _exchangeIcon: function(button, newIcon) { | |
556973c1 MW |
4571 | newIcon = newIcon || 'spinner'; |
4572 | button.removeClass('icon-chevron-down icon-chevron-right icon-spinner').addClass('icon-' + newIcon); | |
878d0d80 AE |
4573 | }, |
4574 | ||
4575 | /** | |
4576 | * Returns the object id for current container. | |
4577 | * | |
4578 | * @param integer containerID | |
4579 | * @return integer | |
4580 | */ | |
eac3b734 MS |
4581 | _getObjectID: function(containerID) { |
4582 | return $('#' + containerID).data('objectID'); | |
4583 | }, | |
878d0d80 | 4584 | |
88a85f47 MW |
4585 | /** |
4586 | * Returns additional parameters. | |
4587 | * | |
4588 | * @param integer containerID | |
4589 | * @return object | |
4590 | */ | |
4591 | _getAdditionalParameters: function(containerID) { | |
4592 | return {}; | |
4593 | }, | |
4594 | ||
77c43423 MW |
4595 | /** |
4596 | * Updates container content. | |
4597 | * | |
4598 | * @param integer containerID | |
4599 | * @param string newContent | |
4600 | * @param string newState | |
4601 | */ | |
4602 | _updateContent: function(containerID, newContent, newState) { | |
4603 | this._containerData[containerID].target.html(newContent); | |
4604 | }, | |
4605 | ||
878d0d80 AE |
4606 | /** |
4607 | * Sets content upon successfull AJAX request. | |
4608 | * | |
4609 | * @param object data | |
4610 | * @param string textStatus | |
4611 | * @param jQuery jqXHR | |
4612 | */ | |
4613 | _success: function(data, textStatus, jqXHR) { | |
4614 | // validate container id | |
4615 | if (!data.returnValues.containerID) return; | |
4616 | var $containerID = data.returnValues.containerID; | |
4617 | ||
4618 | // check if container id is known | |
4619 | if (!this._containers[$containerID]) return; | |
4620 | ||
4621 | // update content storage | |
4622 | this._containerData[$containerID].isOpen = (data.returnValues.isOpen) ? true : false; | |
9fd51bd2 | 4623 | var $newState = (data.returnValues.isOpen) ? 'open' : 'close'; |
878d0d80 AE |
4624 | |
4625 | // update container content | |
b418da85 | 4626 | this._updateContent($containerID, $.trim(data.returnValues.content), $newState); |
b8a3ccb7 MW |
4627 | |
4628 | // update icon | |
556973c1 | 4629 | this._exchangeIcon(this._containerData[$containerID].button, (data.returnValues.isOpen ? 'chevron-down' : 'chevron-right')); |
c4b3ae32 AE |
4630 | } |
4631 | }); | |
4632 | ||
4633 | /** | |
4634 | * Basic implementation for collapsible containers with AJAX support. Requires collapsible | |
4635 | * content to be available in DOM already, if you want to load content on the fly use | |
4636 | * WCF.Collapsible.Remote instead. | |
4637 | */ | |
4638 | WCF.Collapsible.SimpleRemote = WCF.Collapsible.Remote.extend({ | |
4639 | /** | |
4640 | * Initializes an AJAX-based collapsible handler. | |
4641 | * | |
4642 | * @param string className | |
4643 | */ | |
4644 | init: function(className) { | |
4645 | this._super(className); | |
4646 | ||
4647 | // override settings for action proxy | |
4648 | this._proxy = new WCF.Action.Proxy({ | |
4649 | showLoadingOverlay: false | |
4650 | }); | |
4651 | }, | |
4652 | ||
f19a9976 AE |
4653 | /** |
4654 | * @see WCF.Collapsible.Remote._initContainer() | |
4655 | */ | |
4656 | _initContainer: function(containerID) { | |
4657 | this._super(containerID); | |
4658 | ||
4659 | // hide container on init if applicable | |
4660 | if (!this._containerData[containerID].isOpen) { | |
4661 | this._containerData[containerID].target.hide(); | |
143c5f6d | 4662 | this._exchangeIcon(this._containerData[containerID].button, 'chevron-right'); |
f19a9976 AE |
4663 | } |
4664 | }, | |
4665 | ||
c4b3ae32 AE |
4666 | /** |
4667 | * Toggles container visibility. | |
4668 | * | |
4669 | * @param object event | |
4670 | */ | |
4671 | _toggleContainer: function(event) { | |
4672 | var $button = $(event.currentTarget); | |
4673 | var $containerID = $button.data('containerID'); | |
4674 | var $isOpen = this._containerData[$containerID].isOpen; | |
4675 | var $currentState = ($isOpen) ? 'open' : 'close'; | |
4676 | var $newState = ($isOpen) ? 'close' : 'open'; | |
4677 | ||
4678 | this._proxy.setOption('data', { | |
4679 | actionName: 'toggleContainer', | |
4680 | className: this._className, | |
c05528d8 | 4681 | interfaceName: 'wcf\\data\\IToggleContainerAction', |
c4b3ae32 | 4682 | objectIDs: [ this._getObjectID($containerID) ], |
11c6026b | 4683 | parameters: $.extend(true, { |
c4b3ae32 AE |
4684 | containerID: $containerID, |
4685 | currentState: $currentState, | |
4686 | newState: $newState | |
11c6026b | 4687 | }, this._getAdditionalParameters($containerID)) |
c4b3ae32 AE |
4688 | }); |
4689 | this._proxy.sendRequest(); | |
4690 | ||
4691 | // exchange icon | |
143c5f6d | 4692 | this._exchangeIcon(this._containerData[$containerID].button, ($newState === 'open' ? 'chevron-down' : 'chevron-right')); |
6ea6d38b AE |
4693 | |
4694 | // toggle container | |
4695 | if ($newState === 'open') { | |
4696 | this._containerData[$containerID].target.show(); | |
4697 | } | |
4698 | else { | |
4699 | this._containerData[$containerID].target.hide(); | |
4700 | } | |
4701 | ||
2851eadd MS |
4702 | // toogle 'jsCollapsed' CSS class |
4703 | $('#' + $containerID).toggleClass('jsCollapsed'); | |
4704 | ||
6ea6d38b AE |
4705 | // update container data |
4706 | this._containerData[$containerID].isOpen = ($newState === 'open' ? true : false); | |
878d0d80 AE |
4707 | } |
4708 | }); | |
9abd2c89 | 4709 | |
168a9753 AE |
4710 | /** |
4711 | * Provides collapsible sidebars with persistency support. | |
4712 | */ | |
9abd2c89 | 4713 | WCF.Collapsible.Sidebar = Class.extend({ |
168a9753 AE |
4714 | /** |
4715 | * trigger button object | |
4716 | * @var jQuery | |
4717 | */ | |
9abd2c89 | 4718 | _button: null, |
168a9753 | 4719 | |
a3574790 AE |
4720 | /** |
4721 | * trigger button height | |
4722 | * @var integer | |
4723 | */ | |
4724 | _buttonHeight: 0, | |
4725 | ||
168a9753 AE |
4726 | /** |
4727 | * sidebar state | |
4728 | * @var boolean | |
4729 | */ | |
9abd2c89 | 4730 | _isOpen: false, |
168a9753 | 4731 | |
a3574790 AE |
4732 | /** |
4733 | * main container object | |
4734 | * @var jQuery | |
4735 | */ | |
4736 | _mainContainer: null, | |
4737 | ||
168a9753 AE |
4738 | /** |
4739 | * action proxy | |
4740 | * @var WCF.Action.Proxy | |
4741 | */ | |
9abd2c89 | 4742 | _proxy: null, |
168a9753 AE |
4743 | |
4744 | /** | |
4745 | * sidebar object | |
4746 | * @var jQuery | |
4747 | */ | |
9abd2c89 | 4748 | _sidebar: null, |
168a9753 | 4749 | |
a3574790 AE |
4750 | /** |
4751 | * sidebar height | |
4752 | * @var integer | |
4753 | */ | |
4754 | _sidebarHeight: 0, | |
4755 | ||
168a9753 AE |
4756 | /** |
4757 | * sidebar identifier | |
4758 | * @var string | |
4759 | */ | |
9abd2c89 AE |
4760 | _sidebarName: '', |
4761 | ||
a3574790 AE |
4762 | /** |
4763 | * sidebar offset from document top | |
4764 | * @var integer | |
4765 | */ | |
4766 | _sidebarOffset: 0, | |
4767 | ||
4768 | /** | |
4769 | * user panel height | |
4770 | * @var integer | |
4771 | */ | |
4772 | _userPanelHeight: 0, | |
4773 | ||
168a9753 AE |
4774 | /** |
4775 | * Creates a new WCF.Collapsible.Sidebar object. | |
4776 | */ | |
9abd2c89 AE |
4777 | init: function() { |
4778 | this._sidebar = $('.sidebar:eq(0)'); | |
4779 | if (!this._sidebar.length) { | |
4780 | console.debug("[WCF.Collapsible.Sidebar] Could not find sidebar, aborting."); | |
4781 | return; | |
4782 | } | |
4783 | ||
a3574790 | 4784 | this._isOpen = (this._sidebar.data('isOpen')) ? true : false; |
9abd2c89 | 4785 | this._sidebarName = this._sidebar.data('sidebarName'); |
a3574790 AE |
4786 | this._mainContainer = $('#main'); |
4787 | this._sidebarHeight = this._sidebar.height(); | |
4788 | this._sidebarOffset = this._sidebar.getOffsets('offset').top; | |
4789 | this._userPanelHeight = $('#topMenu').outerHeight(); | |
9abd2c89 AE |
4790 | |
4791 | // add toggle button | |
4792 | this._button = $('<a class="collapsibleButton jsTooltip" title="' + WCF.Language.get('wcf.global.button.collapsible') + '" />').prependTo(this._sidebar); | |
35c14679 | 4793 | this._button.wrap('<span />'); |
9abd2c89 | 4794 | this._button.click($.proxy(this._click, this)); |
a3574790 | 4795 | this._buttonHeight = this._button.outerHeight(); |
42d7d2cc AE |
4796 | |
4797 | WCF.DOMNodeInsertedHandler.execute(); | |
9abd2c89 AE |
4798 | |
4799 | this._proxy = new WCF.Action.Proxy({ | |
a3574790 AE |
4800 | showLoadingOverlay: false, |
4801 | url: 'index.php/AJAXInvoke/?t=' + SECURITY_TOKEN + SID_ARG_2ND | |
9abd2c89 AE |
4802 | }); |
4803 | ||
a3574790 AE |
4804 | $(document).scroll($.proxy(this._scroll, this)).resize($.proxy(this._scroll, this)); |
4805 | ||
9abd2c89 | 4806 | this._renderSidebar(); |
a3574790 | 4807 | this._scroll(); |
bcf8bc61 AE |
4808 | |
4809 | // fake resize event once transition has completed | |
4810 | var $window = $(window); | |
4811 | this._sidebar.on('webkitTransitionEnd transitionend msTransitionEnd oTransitionEnd', function() { $window.trigger('resize'); }); | |
9abd2c89 AE |
4812 | }, |
4813 | ||
168a9753 AE |
4814 | /** |
4815 | * Handles clicks on the trigger button. | |
4816 | */ | |
9abd2c89 AE |
4817 | _click: function() { |
4818 | this._isOpen = (this._isOpen) ? false : true; | |
4819 | ||
4820 | this._proxy.setOption('data', { | |
168a9753 AE |
4821 | actionName: 'toggle', |
4822 | className: 'wcf\\system\\user\\collapsible\\content\\UserCollapsibleSidebarHandler', | |
9abd2c89 AE |
4823 | isOpen: (this._isOpen ? 1 : 0), |
4824 | sidebarName: this._sidebarName | |
4825 | }); | |
4826 | this._proxy.sendRequest(); | |
4827 | ||
4828 | this._renderSidebar(); | |
4829 | }, | |
4830 | ||
a3574790 AE |
4831 | /** |
4832 | * Aligns the toggle button upon scroll or resize. | |
4833 | */ | |
4834 | _scroll: function() { | |
4835 | var $window = $(window); | |
4836 | var $scrollOffset = $window.scrollTop(); | |
4837 | ||
4838 | // calculate top and bottom coordinates of visible sidebar | |
4839 | var $topOffset = Math.max($scrollOffset - this._sidebarOffset, 0); | |
4840 | var $bottomOffset = Math.min(this._mainContainer.height(), ($window.height() + $scrollOffset) - this._sidebarOffset); | |
4841 | ||
4842 | var $buttonTop = 0; | |
4843 | if ($bottomOffset === $topOffset) { | |
4844 | // sidebar not within visible area | |
4845 | $buttonTop = this._sidebarOffset + this._sidebarHeight; | |
4846 | } | |
4847 | else { | |
4848 | $buttonTop = $topOffset + (($bottomOffset - $topOffset) / 2); | |
4849 | ||
4850 | // if the user panel is above the sidebar, substract it's height | |
4851 | var $overlap = Math.max(Math.min($topOffset - this._userPanelHeight, this._userPanelHeight), 0); | |
4852 | if ($overlap > 0) { | |
4853 | $buttonTop += ($overlap / 2); | |
4854 | } | |
4855 | } | |
4856 | ||
4857 | // ensure the button does not exceed bottom boundaries | |
4858 | if (($bottomOffset - $topOffset - this._userPanelHeight) < this._buttonHeight) { | |
4859 | $buttonTop = $buttonTop - this._buttonHeight; | |
4860 | } | |
4861 | else { | |
4862 | // exclude half button height | |
4863 | $buttonTop = Math.max($buttonTop - (this._buttonHeight / 2), 0); | |
4864 | } | |
4865 | ||
4866 | this._button.css({ top: $buttonTop + 'px' }); | |
4867 | ||
4868 | }, | |
4869 | ||
168a9753 AE |
4870 | /** |
4871 | * Renders the sidebar state. | |
4872 | */ | |
9abd2c89 AE |
4873 | _renderSidebar: function() { |
4874 | if (this._isOpen) { | |
f2185f99 | 4875 | $('.sidebarOrientationLeft, .sidebarOrientationRight').removeClass('sidebarCollapsed'); |
9abd2c89 AE |
4876 | } |
4877 | else { | |
f2185f99 | 4878 | $('.sidebarOrientationLeft, .sidebarOrientationRight').addClass('sidebarCollapsed'); |
9abd2c89 | 4879 | } |
f70b8234 AE |
4880 | |
4881 | // update button position | |
4882 | this._scroll(); | |
bcf8bc61 AE |
4883 | |
4884 | // IE9 does not support transitions, fire resize event manually | |
4885 | if ($.browser.msie && $.browser.version.indexOf('9') === 0) { | |
4886 | $(window).trigger('resize'); | |
4887 | } | |
9abd2c89 AE |
4888 | } |
4889 | }); | |
878d0d80 | 4890 | |
fb969237 TD |
4891 | /** |
4892 | * Holds userdata of the current user | |
4893 | */ | |
4894 | WCF.User = { | |
4895 | /** | |
a99ebd63 | 4896 | * id of the active user |
fb969237 TD |
4897 | * @var integer |
4898 | */ | |
4899 | userID: 0, | |
4900 | ||
4901 | /** | |
9f959ced | 4902 | * name of the active user |
fb969237 TD |
4903 | * @var string |
4904 | */ | |
4905 | username: '', | |
4906 | ||
4907 | /** | |
4908 | * Initializes userdata | |
4909 | * | |
4910 | * @param integer userID | |
4911 | * @param string username | |
4912 | */ | |
4913 | init: function(userID, username) { | |
4914 | this.userID = userID; | |
4915 | this.username = username; | |
4916 | } | |
4917 | }; | |
4918 | ||
f84920f4 MW |
4919 | /** |
4920 | * Namespace for effect-related functions. | |
4921 | */ | |
4922 | WCF.Effect = {}; | |
4923 | ||
80e49fec AE |
4924 | /** |
4925 | * Scrolls to a specific element offset, optionally handling menu height. | |
4926 | */ | |
4927 | WCF.Effect.Scroll = Class.extend({ | |
4928 | /** | |
4929 | * Scrolls to a specific element offset. | |
4930 | * | |
4931 | * @param jQuery element | |
4932 | * @param boolean excludeMenuHeight | |
aa86d700 | 4933 | * @param boolean disableAnimation |
80e49fec AE |
4934 | * @return boolean |
4935 | */ | |
aa86d700 | 4936 | scrollTo: function(element, excludeMenuHeight, disableAnimation) { |
80e49fec AE |
4937 | if (!element.length) { |
4938 | return true; | |
4939 | } | |
4940 | ||
db316c8f | 4941 | var $elementOffset = element.getOffsets('offset').top; |
80e49fec AE |
4942 | var $documentHeight = $(document).height(); |
4943 | var $windowHeight = $(window).height(); | |
4944 | ||
4945 | // handles menu height | |
7b509312 | 4946 | /*if (excludeMenuHeight) { |
80e49fec | 4947 | $elementOffset = Math.max($elementOffset - $('#topMenu').outerHeight(), 0); |
7b509312 | 4948 | }*/ |
80e49fec AE |
4949 | |
4950 | if ($elementOffset > $documentHeight - $windowHeight) { | |
4951 | $elementOffset = $documentHeight - $windowHeight; | |
4952 | if ($elementOffset < 0) { | |
4953 | $elementOffset = 0; | |
4954 | } | |
4955 | } | |
4956 | ||
aa86d700 AE |
4957 | if (disableAnimation === true) { |
4958 | $('html,body').scrollTop($elementOffset); | |
4959 | } | |
4960 | else { | |
4961 | $('html,body').animate({ scrollTop: $elementOffset }, 400, function (x, t, b, c, d) { | |
4962 | return -c * ( ( t = t / d - 1 ) * t * t * t - 1) + b; | |
4963 | }); | |
4964 | } | |
80e49fec AE |
4965 | |
4966 | return false; | |
4967 | } | |
4968 | }); | |
4969 | ||
f84920f4 MW |
4970 | /** |
4971 | * Creates a smooth scroll effect. | |
4972 | */ | |
80e49fec | 4973 | WCF.Effect.SmoothScroll = WCF.Effect.Scroll.extend({ |
f84920f4 MW |
4974 | /** |
4975 | * Initializes effect. | |
4976 | */ | |
4977 | init: function() { | |
80e49fec | 4978 | var self = this; |
e0b74ad2 | 4979 | $(document).on('click', 'a[href$=#top],a[href$=#bottom]', function() { |
f84920f4 | 4980 | var $target = $(this.hash); |
80e49fec | 4981 | self.scrollTo($target, true); |
1b9cadb9 AE |
4982 | |
4983 | return false; | |
f84920f4 MW |
4984 | }); |
4985 | } | |
80e49fec | 4986 | }); |
f84920f4 | 4987 | |
c39544e5 | 4988 | /** |
8f3284a3 | 4989 | * Creates the balloon tool-tip. |
c39544e5 | 4990 | */ |
39e27190 | 4991 | WCF.Effect.BalloonTooltip = Class.extend({ |
e68f4d0d AE |
4992 | /** |
4993 | * initialization state | |
4994 | * @var boolean | |
4995 | */ | |
4996 | _didInit: false, | |
9f959ced | 4997 | |
e68f4d0d AE |
4998 | /** |
4999 | * tooltip element | |
5000 | * @var jQuery | |
5001 | */ | |
5002 | _tooltip: null, | |
9f959ced | 5003 | |
e68f4d0d AE |
5004 | /** |
5005 | * cache viewport dimensions | |
5006 | * @var object | |
5007 | */ | |
5008 | _viewportDimensions: { }, | |
9f959ced | 5009 | |
e68f4d0d AE |
5010 | /** |
5011 | * Initializes tooltips. | |
5012 | */ | |
c39544e5 | 5013 | init: function() { |
3f42eecb | 5014 | if (jQuery.browser.mobile) return; |
05011566 | 5015 | |
e68f4d0d AE |
5016 | if (!this._didInit) { |
5017 | // create empty div | |
184a8d6d | 5018 | this._tooltip = $('<div id="balloonTooltip" class="balloonTooltip"><span id="balloonTooltipText"></span><span class="pointer"><span></span></span></div>').appendTo($('body')).hide(); |
9f959ced | 5019 | |
e68f4d0d AE |
5020 | // get viewport dimensions |
5021 | this._updateViewportDimensions(); | |
9f959ced | 5022 | |
e68f4d0d AE |
5023 | // update viewport dimensions on resize |
5024 | $(window).resize($.proxy(this._updateViewportDimensions, this)); | |
9f959ced | 5025 | |
e68f4d0d | 5026 | // observe DOM changes |
4bf9feab | 5027 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Effect.BalloonTooltip', $.proxy(this.init, this)); |
9f959ced | 5028 | |
e68f4d0d AE |
5029 | this._didInit = true; |
5030 | } | |
5031 | ||
c39544e5 | 5032 | // init elements |
72065432 | 5033 | $('.jsTooltip').each($.proxy(this._initTooltip, this)); |
e68f4d0d | 5034 | }, |
9f959ced | 5035 | |
e68f4d0d AE |
5036 | /** |
5037 | * Updates cached viewport dimensions. | |
5038 | */ | |
5039 | _updateViewportDimensions: function() { | |
5040 | this._viewportDimensions = $(document).getDimensions(); | |
c39544e5 MW |
5041 | }, |
5042 | ||
e68f4d0d AE |
5043 | /** |
5044 | * Initializes a tooltip element. | |
5045 | * | |
5046 | * @param integer index | |
5047 | * @param object element | |
5048 | */ | |
5049 | _initTooltip: function(index, element) { | |
5050 | var $element = $(element); | |
c39544e5 | 5051 | |
72065432 L |
5052 | if ($element.hasClass('jsTooltip')) { |
5053 | $element.removeClass('jsTooltip'); | |
e68f4d0d | 5054 | var $title = $element.attr('title'); |
9f959ced | 5055 | |
e68f4d0d AE |
5056 | // ignore empty elements |
5057 | if ($title !== '') { | |
5058 | $element.data('tooltip', $title); | |
f4126129 | 5059 | $element.removeAttr('title'); |
9f959ced | 5060 | |
e68f4d0d AE |
5061 | $element.hover( |
5062 | $.proxy(this._mouseEnterHandler, this), | |
5063 | $.proxy(this._mouseLeaveHandler, this) | |
5064 | ); | |
8cfb8ca1 | 5065 | $element.click($.proxy(this._mouseLeaveHandler, this)); |
e68f4d0d AE |
5066 | } |
5067 | } | |
c39544e5 MW |
5068 | }, |
5069 | ||
e68f4d0d AE |
5070 | /** |
5071 | * Shows tooltip on hover. | |
5072 | * | |
5073 | * @param object event | |
5074 | */ | |
c39544e5 MW |
5075 | _mouseEnterHandler: function(event) { |
5076 | var $element = $(event.currentTarget); | |
c39544e5 | 5077 | |
93430cba | 5078 | var $title = $element.attr('title'); |
ea2294bc TD |
5079 | if ($title && $title !== '') { |
5080 | $element.data('tooltip', $title); | |
5081 | $element.removeAttr('title'); | |
5082 | } | |
93430cba | 5083 | |
8cfb8ca1 MW |
5084 | // reset tooltip position |
5085 | this._tooltip.css({ | |
5086 | top: "0px", | |
5087 | left: "0px" | |
5088 | }); | |
5089 | ||
e81ca6a1 MW |
5090 | // empty tooltip, skip |
5091 | if (!$element.data('tooltip')) { | |
5092 | this._tooltip.hide(); | |
5093 | return; | |
5094 | } | |
5095 | ||
e68f4d0d AE |
5096 | // update text |
5097 | this._tooltip.children('span:eq(0)').text($element.data('tooltip')); | |
9f959ced | 5098 | |
e68f4d0d | 5099 | // get arrow |
ef097134 | 5100 | var $arrow = this._tooltip.find('.pointer'); |
9f959ced | 5101 | |
e68f4d0d AE |
5102 | // get arrow width |
5103 | this._tooltip.show(); | |
5104 | var $arrowWidth = $arrow.outerWidth(); | |
5105 | this._tooltip.hide(); | |
24c42d67 | 5106 | |
e68f4d0d AE |
5107 | // calculate position |
5108 | var $elementOffsets = $element.getOffsets('offset'); | |
5109 | var $elementDimensions = $element.getDimensions('outer'); | |
5110 | var $tooltipDimensions = this._tooltip.getDimensions('outer'); | |
5111 | var $tooltipDimensionsInner = this._tooltip.getDimensions('inner'); | |
9f959ced | 5112 | |
e68f4d0d AE |
5113 | var $elementCenter = $elementOffsets.left + Math.ceil($elementDimensions.width / 2); |
5114 | var $tooltipHalfWidth = Math.ceil($tooltipDimensions.width / 2); | |
257248cd | 5115 | |
e68f4d0d | 5116 | // determine alignment |
9f959ced | 5117 | var $alignment = 'center'; |
e68f4d0d AE |
5118 | if (($elementCenter - $tooltipHalfWidth) < 5) { |
5119 | $alignment = 'left'; | |
5120 | } | |
5121 | else if ((this._viewportDimensions.width - 5) < ($elementCenter + $tooltipHalfWidth)) { | |
5122 | $alignment = 'right'; | |
1fe125ca | 5123 | } |
9f959ced | 5124 | |
e68f4d0d | 5125 | // calculate top offset |
c8ee6af8 | 5126 | if ($elementOffsets.top + $elementDimensions.height + $tooltipDimensions.height - $(document).scrollTop() < $(window).height()) { |
5127 | var $top = $elementOffsets.top + $elementDimensions.height + 7; | |
5128 | this._tooltip.removeClass('inverse'); | |
5129 | $arrow.css('top', -5); | |
5130 | } | |
5131 | else { | |
08a3a752 | 5132 | var $top = $elementOffsets.top - $tooltipDimensions.height - 7; |
c8ee6af8 | 5133 | this._tooltip.addClass('inverse'); |
5134 | $arrow.css('top', $tooltipDimensions.height); | |
5135 | } | |
9f959ced | 5136 | |
e68f4d0d AE |
5137 | // calculate left offset |
5138 | switch ($alignment) { | |
5139 | case 'center': | |
5140 | var $left = Math.round($elementOffsets.left - $tooltipHalfWidth + ($elementDimensions.width / 2)); | |
9f959ced | 5141 | |
e68f4d0d AE |
5142 | $arrow.css({ |
5143 | left: ($tooltipDimensionsInner.width / 2 - $arrowWidth / 2) + "px" | |
5144 | }); | |
5145 | break; | |
9f959ced | 5146 | |
e68f4d0d AE |
5147 | case 'left': |
5148 | var $left = $elementOffsets.left; | |
9f959ced | 5149 | |
e68f4d0d | 5150 | $arrow.css({ |
270a205a | 5151 | left: "5px" |
e68f4d0d AE |
5152 | }); |
5153 | break; | |
9f959ced | 5154 | |
e68f4d0d AE |
5155 | case 'right': |
5156 | var $left = $elementOffsets.left + $elementDimensions.width - $tooltipDimensions.width; | |
9f959ced | 5157 | |
e68f4d0d | 5158 | $arrow.css({ |
270a205a | 5159 | left: ($tooltipDimensionsInner.width - $arrowWidth - 5) + "px" |
e68f4d0d AE |
5160 | }); |
5161 | break; | |
5162 | } | |
9f959ced | 5163 | |
e68f4d0d AE |
5164 | // move tooltip |
5165 | this._tooltip.css({ | |
5166 | top: $top + "px", | |
5167 | left: $left + "px" | |
5168 | }); | |
5169 | ||
5170 | // show tooltip | |
90a8839c | 5171 | this._tooltip.wcfFadeIn(); |
2875507b MW |
5172 | }, |
5173 | ||
e68f4d0d AE |
5174 | /** |
5175 | * Hides tooltip once cursor left the element. | |
5176 | * | |
5177 | * @param object event | |
5178 | */ | |
2875507b | 5179 | _mouseLeaveHandler: function(event) { |
90a8839c MW |
5180 | this._tooltip.stop().hide().css({ |
5181 | opacity: 1 | |
5182 | }); | |
c39544e5 | 5183 | } |
39e27190 | 5184 | }); |
c39544e5 | 5185 | |
b3991cb3 AE |
5186 | /** |
5187 | * Handles clicks outside an overlay, hitting body-tag through bubbling. | |
5188 | * | |
5189 | * You should always remove callbacks before disposing the attached element, | |
5190 | * preventing errors from blocking the iteration. Furthermore you should | |
5191 | * always handle clicks on your overlay's container and return 'false' to | |
5192 | * prevent bubbling. | |
5193 | */ | |
5194 | WCF.CloseOverlayHandler = { | |
5195 | /** | |
5196 | * list of callbacks | |
5197 | * @var WCF.Dictionary | |
5198 | */ | |
5199 | _callbacks: new WCF.Dictionary(), | |
9f959ced | 5200 | |
b3991cb3 AE |
5201 | /** |
5202 | * indicates that overlay handler is listening to click events on body-tag | |
5203 | * @var boolean | |
5204 | */ | |
5205 | _isListening: false, | |
9f959ced | 5206 | |
b3991cb3 AE |
5207 | /** |
5208 | * Adds a new callback. | |
5209 | * | |
5210 | * @param string identifier | |
5211 | * @param object callback | |
5212 | */ | |
5213 | addCallback: function(identifier, callback) { | |
5214 | this._bindListener(); | |
83428b7f | 5215 | |
b3991cb3 | 5216 | if (this._callbacks.isset(identifier)) { |
78e4d558 | 5217 | console.debug("[WCF.CloseOverlayHandler] identifier '" + identifier + "' is already bound to a callback"); |
b3991cb3 AE |
5218 | return false; |
5219 | } | |
9f959ced | 5220 | |
b3991cb3 AE |
5221 | this._callbacks.add(identifier, callback); |
5222 | }, | |
9f959ced | 5223 | |
b3991cb3 AE |
5224 | /** |
5225 | * Removes a callback from list. | |
5226 | * | |
5227 | * @param string identifier | |
5228 | */ | |
5229 | removeCallback: function(identifier) { | |
5230 | if (this._callbacks.isset(identifier)) { | |
5231 | this._callbacks.remove(identifier); | |
5232 | } | |
5233 | }, | |
9f959ced | 5234 | |
b3991cb3 AE |
5235 | /** |
5236 | * Binds click event handler. | |
5237 | */ | |
5238 | _bindListener: function() { | |
5239 | if (this._isListening) return; | |
9f959ced | 5240 | |
b3991cb3 | 5241 | $('body').click($.proxy(this._executeCallbacks, this)); |
9f959ced | 5242 | |
b3991cb3 AE |
5243 | this._isListening = true; |
5244 | }, | |
9f959ced | 5245 | |
b3991cb3 AE |
5246 | /** |
5247 | * Executes callbacks on click. | |
5248 | */ | |
83428b7f | 5249 | _executeCallbacks: function(event) { |
b3991cb3 AE |
5250 | this._callbacks.each(function(pair) { |
5251 | // execute callback | |
5252 | pair.value(); | |
5253 | }); | |
5254 | } | |
5255 | }; | |
5256 | ||
9c1e5045 AE |
5257 | /** |
5258 | * Notifies objects once a DOM node was inserted. | |
5259 | */ | |
5260 | WCF.DOMNodeInsertedHandler = { | |
5261 | /** | |
5262 | * list of callbacks | |
0e6979fb | 5263 | * @var array<object> |
9c1e5045 | 5264 | */ |
0e6979fb | 5265 | _callbacks: [ ], |
597e2774 | 5266 | |
9c1e5045 AE |
5267 | /** |
5268 | * prevent infinite loop if a callback manipulates DOM | |
5269 | * @var boolean | |
5270 | */ | |
5271 | _isExecuting: false, | |
597e2774 | 5272 | |
9c1e5045 AE |
5273 | /** |
5274 | * Adds a new callback. | |
5275 | * | |
5276 | * @param string identifier | |
5277 | * @param object callback | |
5278 | */ | |
5279 | addCallback: function(identifier, callback) { | |
0e6979fb | 5280 | this._callbacks.push(callback); |
9c1e5045 | 5281 | }, |
597e2774 | 5282 | |
9c1e5045 AE |
5283 | /** |
5284 | * Executes callbacks on click. | |
5285 | */ | |
d371330f | 5286 | _executeCallbacks: function() { |
42d7d2cc | 5287 | if (this._isExecuting) return; |
597e2774 | 5288 | |
901393e3 | 5289 | // do not track events while executing callbacks |
9c1e5045 | 5290 | this._isExecuting = true; |
901393e3 | 5291 | |
0e6979fb AE |
5292 | for (var $i = 0, $length = this._callbacks.length; $i < $length; $i++) { |
5293 | this._callbacks[$i](); | |
5294 | } | |
901393e3 AE |
5295 | |
5296 | // enable listener again | |
5297 | this._isExecuting = false; | |
597e2774 AE |
5298 | }, |
5299 | ||
5300 | /** | |
42d7d2cc | 5301 | * Executes all callbacks. |
d371330f | 5302 | */ |
42d7d2cc | 5303 | execute: function() { |
d371330f | 5304 | this._executeCallbacks(); |
9c1e5045 AE |
5305 | } |
5306 | }; | |
5307 | ||
809e4170 MS |
5308 | /** |
5309 | * Notifies objects once a DOM node was removed. | |
5310 | */ | |
5311 | WCF.DOMNodeRemovedHandler = { | |
5312 | /** | |
5313 | * list of callbacks | |
5314 | * @var WCF.Dictionary | |
5315 | */ | |
5316 | _callbacks: new WCF.Dictionary(), | |
9f959ced | 5317 | |
809e4170 MS |
5318 | /** |
5319 | * prevent infinite loop if a callback manipulates DOM | |
5320 | * @var boolean | |
5321 | */ | |
5322 | _isExecuting: false, | |
9f959ced | 5323 | |
809e4170 MS |
5324 | /** |
5325 | * indicates that overlay handler is listening to DOMNodeRemoved events on body-tag | |
5326 | * @var boolean | |
5327 | */ | |
5328 | _isListening: false, | |
9f959ced | 5329 | |
809e4170 MS |
5330 | /** |
5331 | * Adds a new callback. | |
5332 | * | |
5333 | * @param string identifier | |
5334 | * @param object callback | |
5335 | */ | |
5336 | addCallback: function(identifier, callback) { | |
5337 | this._bindListener(); | |
9f959ced | 5338 | |
809e4170 MS |
5339 | if (this._callbacks.isset(identifier)) { |
5340 | console.debug("[WCF.DOMNodeRemovedHandler] identifier '" + identifier + "' is already bound to a callback"); | |
5341 | return false; | |
5342 | } | |
9f959ced | 5343 | |
809e4170 MS |
5344 | this._callbacks.add(identifier, callback); |
5345 | }, | |
9f959ced | 5346 | |
809e4170 MS |
5347 | /** |
5348 | * Removes a callback from list. | |
5349 | * | |
5350 | * @param string identifier | |
5351 | */ | |
5352 | removeCallback: function(identifier) { | |
5353 | if (this._callbacks.isset(identifier)) { | |
5354 | this._callbacks.remove(identifier); | |
5355 | } | |
5356 | }, | |
9f959ced | 5357 | |
809e4170 MS |
5358 | /** |
5359 | * Binds click event handler. | |
5360 | */ | |
5361 | _bindListener: function() { | |
5362 | if (this._isListening) return; | |
9f959ced | 5363 | |
809e4170 | 5364 | $(document).bind('DOMNodeRemoved', $.proxy(this._executeCallbacks, this)); |
9f959ced | 5365 | |
809e4170 MS |
5366 | this._isListening = true; |
5367 | }, | |
9f959ced | 5368 | |
809e4170 MS |
5369 | /** |
5370 | * Executes callbacks if a DOM node is removed. | |
5371 | */ | |
5372 | _executeCallbacks: function(event) { | |
5373 | if (this._isExecuting) return; | |
9f959ced | 5374 | |
809e4170 MS |
5375 | // do not track events while executing callbacks |
5376 | this._isExecuting = true; | |
5377 | ||
5378 | this._callbacks.each(function(pair) { | |
5379 | // execute callback | |
5380 | pair.value(event); | |
5381 | }); | |
5382 | ||
5383 | // enable listener again | |
5384 | this._isExecuting = false; | |
5385 | } | |
5386 | }; | |
5387 | ||
03fcd560 AE |
5388 | WCF.PageVisibilityHandler = { |
5389 | /** | |
5390 | * list of callbacks | |
5391 | * @var WCF.Dictionary | |
5392 | */ | |
5393 | _callbacks: new WCF.Dictionary(), | |
5394 | ||
5395 | /** | |
5396 | * indicates that event listeners are bound | |
5397 | * @var boolean | |
5398 | */ | |
5399 | _isListening: false, | |
5400 | ||
5401 | /** | |
5402 | * name of window's hidden property | |
5403 | * @var string | |
5404 | */ | |
5405 | _hiddenFieldName: '', | |
5406 | ||
5407 | /** | |
5408 | * Adds a new callback. | |
5409 | * | |
5410 | * @param string identifier | |
5411 | * @param object callback | |
5412 | */ | |
5413 | addCallback: function(identifier, callback) { | |
5414 | this._bindListener(); | |
5415 | ||
5416 | if (this._callbacks.isset(identifier)) { | |
5417 | console.debug("[WCF.PageVisibilityHandler] identifier '" + identifier + "' is already bound to a callback"); | |
5418 | return false; | |
5419 | } | |
5420 | ||
5421 | this._callbacks.add(identifier, callback); | |
5422 | }, | |
5423 | ||
5424 | /** | |
5425 | * Removes a callback from list. | |
5426 | * | |
5427 | * @param string identifier | |
5428 | */ | |
5429 | removeCallback: function(identifier) { | |
5430 | if (this._callbacks.isset(identifier)) { | |
5431 | this._callbacks.remove(identifier); | |
5432 | } | |
5433 | }, | |
5434 | ||
5435 | /** | |
5436 | * Binds click event handler. | |
5437 | */ | |
5438 | _bindListener: function() { | |
5439 | if (this._isListening) return; | |
5440 | ||
5441 | var $eventName = null; | |
5442 | if (typeof document.hidden !== "undefined") { | |
5443 | this._hiddenFieldName = "hidden"; | |
5444 | $eventName = "visibilitychange"; | |
5445 | } | |
5446 | else if (typeof document.mozHidden !== "undefined") { | |
5447 | this._hiddenFieldName = "mozHidden"; | |
5448 | $eventName = "mozvisibilitychange"; | |
5449 | } | |
5450 | else if (typeof document.msHidden !== "undefined") { | |
5451 | this._hiddenFieldName = "msHidden"; | |
5452 | $eventName = "msvisibilitychange"; | |
5453 | } | |
5454 | else if (typeof document.webkitHidden !== "undefined") { | |
5455 | this._hiddenFieldName = "webkitHidden"; | |
5456 | $eventName = "webkitvisibilitychange"; | |
5457 | } | |
5458 | ||
5459 | if ($eventName === null) { | |
5460 | console.debug("[WCF.PageVisibilityHandler] This browser does not support the page visibility API."); | |
5461 | } | |
5462 | else { | |
5463 | $(document).on($eventName, $.proxy(this._executeCallbacks, this)); | |
5464 | } | |
5465 | ||
5466 | this._isListening = true; | |
5467 | }, | |
5468 | ||
5469 | /** | |
5470 | * Executes callbacks if page is hidden/visible again. | |
5471 | */ | |
5472 | _executeCallbacks: function(event) { | |
5473 | if (this._isExecuting) return; | |
5474 | ||
5475 | // do not track events while executing callbacks | |
5476 | this._isExecuting = true; | |
5477 | ||
5478 | var $state = document[this._hiddenFieldName]; | |
5479 | this._callbacks.each(function(pair) { | |
5480 | // execute callback | |
5481 | pair.value($state); | |
5482 | }); | |
5483 | ||
5484 | // enable listener again | |
5485 | this._isExecuting = false; | |
5486 | } | |
5487 | }; | |
5488 | ||
809e4170 MS |
5489 | /** |
5490 | * Namespace for table related classes. | |
5491 | */ | |
5492 | WCF.Table = {}; | |
5493 | ||
5494 | /** | |
5495 | * Handles empty tables which can be used in combination with WCF.Action.Proxy. | |
5496 | */ | |
5497 | WCF.Table.EmptyTableHandler = Class.extend({ | |
5498 | /** | |
5499 | * handler options | |
5500 | * @var object | |
5501 | */ | |
5502 | _options: {}, | |
5503 | ||
5504 | /** | |
5505 | * class name of the relevant rows | |
5506 | * @var string | |
5507 | */ | |
5508 | _rowClassName: '', | |
5509 | ||
5510 | /** | |
5511 | * Initalizes a new WCF.Table.EmptyTableHandler object. | |
5512 | * | |
809e4170 | 5513 | * @param jQuery tableContainer |
e0ee2eff | 5514 | * @param string rowClassName |
809e4170 MS |
5515 | * @param object options |
5516 | */ | |
e0ee2eff | 5517 | init: function(tableContainer, rowClassName, options) { |
809e4170 MS |
5518 | this._rowClassName = rowClassName; |
5519 | this._tableContainer = tableContainer; | |
5520 | ||
5521 | this._options = $.extend(true, { | |
5522 | emptyMessage: null, | |
c0bd9c8a | 5523 | messageType: 'info', |
809e4170 MS |
5524 | refreshPage: false, |
5525 | updatePageNumber: false | |
5526 | }, options || { }); | |
5527 | ||
5528 | WCF.DOMNodeRemovedHandler.addCallback('WCF.Table.EmptyTableHandler.' + rowClassName, $.proxy(this._remove, this)); | |
5529 | }, | |
5530 | ||
5531 | /** | |
5532 | * Handles the removal of a DOM node. | |
5533 | */ | |
5534 | _remove: function(event) { | |
5535 | var element = $(event.target); | |
5536 | ||
5537 | // check if DOM element is relevant | |
5538 | if (element.hasClass(this._rowClassName)) { | |
5539 | var tbody = element.parents('tbody:eq(0)'); | |
5540 | ||
5541 | // check if table will be empty if DOM node is removed | |
5542 | if (tbody.children('tr').length == 1) { | |
5543 | if (this._options.emptyMessage) { | |
5544 | // insert message | |
5545 | this._tableContainer.replaceWith($('<p />').addClass(this._options.messageType).text(this._options.emptyMessage)); | |
5546 | } | |
5547 | else if (this._options.refreshPage) { | |
5548 | // refresh page | |
5549 | if (this._options.updatePageNumber) { | |
5550 | // calculate the new page number | |
5551 | var pageNumberURLComponents = window.location.href.match(/(\?|&)pageNo=(\d+)/g); | |
5552 | if (pageNumberURLComponents) { | |
5553 | var currentPageNumber = pageNumberURLComponents[pageNumberURLComponents.length - 1].match(/\d+/g); | |
5554 | if (this._options.updatePageNumber > 0) { | |
5555 | currentPageNumber++; | |
5556 | } | |
5557 | else { | |
5558 | currentPageNumber--; | |
5559 | } | |
5560 | ||
5561 | window.location = window.location.href.replace(pageNumberURLComponents[pageNumberURLComponents.length - 1], pageNumberURLComponents[pageNumberURLComponents.length - 1][0] + 'pageNo=' + currentPageNumber); | |
5562 | } | |
5563 | } | |
5564 | else { | |
5565 | window.location.reload(); | |
5566 | } | |
5567 | } | |
5568 | else { | |
5569 | // simply remove the table container | |
5570 | this._tableContainer.remove(); | |
5571 | } | |
5572 | } | |
5573 | } | |
5574 | } | |
5575 | }); | |
5576 | ||
046e1292 AE |
5577 | /** |
5578 | * Namespace for search related classes. | |
5579 | */ | |
5580 | WCF.Search = {}; | |
5581 | ||
5582 | /** | |
4ad00d80 | 5583 | * Performs a quick search. |
046e1292 | 5584 | */ |
4ad00d80 | 5585 | WCF.Search.Base = Class.extend({ |
046e1292 AE |
5586 | /** |
5587 | * notification callback | |
5588 | * @var object | |
5589 | */ | |
5590 | _callback: null, | |
4ad00d80 | 5591 | |
4d10750a AE |
5592 | /** |
5593 | * class name | |
5594 | * @var string | |
5595 | */ | |
4ad00d80 | 5596 | _className: '', |
c000b08a | 5597 | |
cf55c379 AE |
5598 | /** |
5599 | * comma seperated list | |
5600 | * @var boolean | |
5601 | */ | |
5602 | _commaSeperated: false, | |
5603 | ||
44f8f78b AE |
5604 | /** |
5605 | * delay in miliseconds before a request is send to the server | |
5606 | * @var integer | |
5607 | */ | |
5608 | _delay: 0, | |
5609 | ||
c000b08a MS |
5610 | /** |
5611 | * list with values that are excluded from seaching | |
5612 | * @var array | |
5613 | */ | |
5614 | _excludedSearchValues: [], | |
cf55c379 | 5615 | |
9a130ab4 AE |
5616 | /** |
5617 | * count of available results | |
5618 | * @var integer | |
5619 | */ | |
5620 | _itemCount: 0, | |
5621 | ||
5622 | /** | |
5623 | * item index, -1 if none is selected | |
5624 | * @var integer | |
5625 | */ | |
5626 | _itemIndex: -1, | |
5627 | ||
046e1292 AE |
5628 | /** |
5629 | * result list | |
5630 | * @var jQuery | |
5631 | */ | |
5632 | _list: null, | |
cf55c379 AE |
5633 | |
5634 | /** | |
5635 | * old search string, used for comparison | |
5636 | * @var array<string> | |
5637 | */ | |
5638 | _oldSearchString: [ ], | |
5639 | ||
046e1292 AE |
5640 | /** |
5641 | * action proxy | |
5642 | * @var WCF.Action.Proxy | |
5643 | */ | |
5644 | _proxy: null, | |
cf55c379 | 5645 | |
046e1292 AE |
5646 | /** |
5647 | * search input field | |
5648 | * @var jQuery | |
5649 | */ | |
5650 | _searchInput: null, | |
4d10750a AE |
5651 | |
5652 | /** | |
5653 | * minimum search input length, MUST be 1 or higher | |
5654 | * @var integer | |
5655 | */ | |
fbe99f89 | 5656 | _triggerLength: 3, |
cf55c379 | 5657 | |
44f8f78b AE |
5658 | /** |
5659 | * delay timer | |
5660 | * @var WCF.PeriodicalExecuter | |
5661 | */ | |
5662 | _timer: null, | |
5663 | ||
046e1292 AE |
5664 | /** |
5665 | * Initializes a new search. | |
5666 | * | |
5667 | * @param jQuery searchInput | |
5668 | * @param object callback | |
c000b08a | 5669 | * @param array excludedSearchValues |
cf55c379 | 5670 | * @param boolean commaSeperated |
80da9de3 | 5671 | * @param boolean showLoadingOverlay |
046e1292 | 5672 | */ |
80da9de3 | 5673 | init: function(searchInput, callback, excludedSearchValues, commaSeperated, showLoadingOverlay) { |
b0ce5298 | 5674 | if (callback !== null && callback !== undefined && !$.isFunction(callback)) { |
6f475a52 | 5675 | console.debug("[WCF.Search.Base] The given callback is invalid, aborting."); |
046e1292 AE |
5676 | return; |
5677 | } | |
b0ce5298 AE |
5678 | |
5679 | this._callback = (callback) ? callback : null; | |
44f8f78b | 5680 | this._delay = 0; |
fdd8763a | 5681 | this._excludedSearchValues = []; |
c000b08a MS |
5682 | if (excludedSearchValues) { |
5683 | this._excludedSearchValues = excludedSearchValues; | |
5684 | } | |
71662ae8 AE |
5685 | |
5686 | this._searchInput = $(searchInput); | |
5687 | if (!this._searchInput.length) { | |
5688 | console.debug("[WCF.Search.Base] Selector '" + searchInput + "' for search input is invalid, aborting."); | |
5689 | return; | |
5690 | } | |
5691 | ||
fbdfbaba | 5692 | this._searchInput.keydown($.proxy(this._keyDown, this)).keyup($.proxy(this._keyUp, this)).wrap('<span class="dropdown" />'); |
b986ad3d MS |
5693 | |
5694 | if ($.browser.mozilla && $.browser.touch) { | |
5695 | this._searchInput.on('input', $.proxy(this._keyUp, this)); | |
5696 | } | |
5697 | ||
7f45f320 | 5698 | this._list = $('<ul class="dropdownMenu" />').insertAfter(this._searchInput); |
cf55c379 AE |
5699 | this._commaSeperated = (commaSeperated) ? true : false; |
5700 | this._oldSearchString = [ ]; | |
046e1292 | 5701 | |
9a130ab4 AE |
5702 | this._itemCount = 0; |
5703 | this._itemIndex = -1; | |
5704 | ||
046e1292 | 5705 | this._proxy = new WCF.Action.Proxy({ |
387cd7da | 5706 | showLoadingOverlay: (showLoadingOverlay !== true ? false : true), |
40b173ec MK |
5707 | success: $.proxy(this._success, this), |
5708 | autoAbortPrevious: true | |
046e1292 | 5709 | }); |
b0ce5298 | 5710 | |
404c9abe | 5711 | if (this._searchInput.is('input')) { |
b0ce5298 AE |
5712 | this._searchInput.attr('autocomplete', 'off'); |
5713 | } | |
03e34cc3 AE |
5714 | |
5715 | this._searchInput.blur($.proxy(this._blur, this)); | |
38d131ce AE |
5716 | |
5717 | WCF.Dropdown.initDropdownFragment(this._searchInput.parent(), this._list); | |
03e34cc3 AE |
5718 | }, |
5719 | ||
5720 | /** | |
5721 | * Closes the dropdown after a short delay. | |
5722 | */ | |
5723 | _blur: function() { | |
5724 | var self = this; | |
5725 | new WCF.PeriodicalExecuter(function(pe) { | |
5726 | if (self._list.is(':visible')) { | |
5727 | self._clearList(false); | |
5728 | } | |
5729 | ||
5730 | pe.stop(); | |
369b47cd | 5731 | }, 250); |
046e1292 | 5732 | }, |
4ad00d80 | 5733 | |
fbdfbaba AE |
5734 | /** |
5735 | * Blocks execution of 'Enter' event. | |
5736 | * | |
5737 | * @param object event | |
5738 | */ | |
5739 | _keyDown: function(event) { | |
7c6f7523 AE |
5740 | if (event.which === $.ui.keyCode.ENTER) { |
5741 | var $dropdown = this._searchInput.parents('.dropdown'); | |
5742 | ||
5743 | if ($dropdown.data('disableAutoFocus')) { | |
5744 | if (this._itemIndex !== -1) { | |
5745 | event.preventDefault(); | |
5746 | } | |
c138555b | 5747 | } |
7c6f7523 | 5748 | else if ($dropdown.data('preventSubmit') || this._itemIndex !== -1) { |
c138555b AE |
5749 | event.preventDefault(); |
5750 | } | |
fbdfbaba AE |
5751 | } |
5752 | }, | |
5753 | ||
046e1292 AE |
5754 | /** |
5755 | * Performs a search upon key up. | |
cf55c379 AE |
5756 | * |
5757 | * @param object event | |
046e1292 | 5758 | */ |
cf55c379 | 5759 | _keyUp: function(event) { |
9a130ab4 AE |
5760 | // handle arrow keys and return key |
5761 | switch (event.which) { | |
5762 | case 37: // arrow-left | |
5763 | case 39: // arrow-right | |
5764 | return; | |
5765 | break; | |
5766 | ||
5767 | case 38: // arrow up | |
5768 | this._selectPreviousItem(); | |
5769 | return; | |
5770 | break; | |
5771 | ||
5772 | case 40: // arrow down | |
5773 | this._selectNextItem(); | |
5774 | return; | |
5775 | break; | |
5776 | ||
5777 | case 13: // return key | |
5778 | return this._selectElement(event); | |
5779 | break; | |
5780 | } | |
5781 | ||
cf55c379 | 5782 | var $content = this._getSearchString(event); |
046e1292 AE |
5783 | if ($content === '') { |
5784 | this._clearList(true); | |
5785 | } | |
4d10750a | 5786 | else if ($content.length >= this._triggerLength) { |
4ad00d80 AE |
5787 | var $parameters = { |
5788 | data: { | |
c000b08a | 5789 | excludedSearchValues: this._excludedSearchValues, |
4ad00d80 | 5790 | searchString: $content |
596a5751 | 5791 | } |
4ad00d80 AE |
5792 | }; |
5793 | ||
44f8f78b AE |
5794 | if (this._delay) { |
5795 | if (this._timer !== null) { | |
5796 | this._timer.stop(); | |
5797 | } | |
5798 | ||
5799 | var self = this; | |
5800 | this._timer = new WCF.PeriodicalExecuter(function() { | |
5801 | self._queryServer($parameters); | |
5802 | ||
5803 | self._timer.stop(); | |
5804 | self._timer = null; | |
5805 | }, this._delay); | |
5806 | } | |
5807 | else { | |
5808 | this._queryServer($parameters); | |
5809 | } | |
046e1292 | 5810 | } |
4d10750a AE |
5811 | else { |
5812 | // input below trigger length | |
5813 | this._clearList(false); | |
5814 | } | |
046e1292 | 5815 | }, |
4ad00d80 | 5816 | |
44f8f78b AE |
5817 | /** |
5818 | * Queries the server. | |
5819 | * | |
5820 | * @param object parameters | |
5821 | */ | |
5822 | _queryServer: function(parameters) { | |
5823 | this._searchInput.parents('.searchBar').addClass('loading'); | |
5824 | this._proxy.setOption('data', { | |
5825 | actionName: 'getSearchResultList', | |
5826 | className: this._className, | |
5827 | interfaceName: 'wcf\\data\\ISearchAction', | |
5828 | parameters: this._getParameters(parameters) | |
5829 | }); | |
5830 | this._proxy.sendRequest(); | |
5831 | }, | |
5832 | ||
5833 | /** | |
5834 | * Sets query delay in miliseconds. | |
5835 | * | |
5836 | * @param integer delay | |
5837 | */ | |
5838 | setDelay: function(delay) { | |
5839 | this._delay = delay; | |
5840 | }, | |
5841 | ||
9a130ab4 AE |
5842 | /** |
5843 | * Selects the next item in list. | |
5844 | */ | |
5845 | _selectNextItem: function() { | |
5846 | if (this._itemCount === 0) { | |
5847 | return; | |
5848 | } | |
5849 | ||
5850 | // remove previous marking | |
5851 | this._itemIndex++; | |
5852 | if (this._itemIndex === this._itemCount) { | |
5853 | this._itemIndex = 0; | |
5854 | } | |
5855 | ||
5856 | this._highlightSelectedElement(); | |
5857 | }, | |
5858 | ||
5859 | /** | |
5860 | * Selects the previous item in list. | |
5861 | */ | |
5862 | _selectPreviousItem: function() { | |
5863 | if (this._itemCount === 0) { | |
5864 | return; | |
5865 | } | |
5866 | ||
5867 | this._itemIndex--; | |
5868 | if (this._itemIndex === -1) { | |
5869 | this._itemIndex = this._itemCount - 1; | |
5870 | } | |
5871 | ||
5872 | this._highlightSelectedElement(); | |
5873 | }, | |
5874 | ||
5875 | /** | |
5876 | * Highlights the active item. | |
5877 | */ | |
5878 | _highlightSelectedElement: function() { | |
5879 | this._list.find('li').removeClass('dropdownNavigationItem'); | |
5880 | this._list.find('li:eq(' + this._itemIndex + ')').addClass('dropdownNavigationItem'); | |
5881 | }, | |
5882 | ||
5883 | /** | |
5884 | * Selects the active item by pressing the return key. | |
5885 | * | |
5886 | * @param object event | |
5887 | * @return boolean | |
5888 | */ | |
5889 | _selectElement: function(event) { | |
5890 | if (this._itemCount === 0) { | |
5891 | return true; | |
5892 | } | |
5893 | ||
5894 | this._list.find('li.dropdownNavigationItem').trigger('click'); | |
5895 | ||
5896 | return false; | |
5897 | }, | |
5898 | ||
cf55c379 AE |
5899 | /** |
5900 | * Returns search string. | |
5901 | * | |
5902 | * @return string | |
5903 | */ | |
5904 | _getSearchString: function(event) { | |
5905 | var $searchString = $.trim(this._searchInput.val()); | |
5906 | if (this._commaSeperated) { | |
5907 | var $keyCode = event.keyCode || event.which; | |
3e597f7d AE |
5908 | if ($keyCode == $.ui.keyCode.COMMA) { |
5909 | // ignore event if char is ',' | |
cf55c379 AE |
5910 | return ''; |
5911 | } | |
5912 | ||
5913 | var $current = $searchString.split(','); | |
68097635 AE |
5914 | var $length = $current.length; |
5915 | for (var $i = 0; $i < $length; $i++) { | |
cf55c379 AE |
5916 | // remove whitespaces at the beginning or end |
5917 | $current[$i] = $.trim($current[$i]); | |
68097635 AE |
5918 | } |
5919 | ||
5920 | for (var $i = 0; $i < $length; $i++) { | |
cf55c379 AE |
5921 | var $part = $current[$i]; |
5922 | ||
5923 | if (this._oldSearchString[$i]) { | |
5924 | // compare part | |
5925 | if ($part != this._oldSearchString[$i]) { | |
5926 | // current part was changed | |
5927 | $searchString = $part; | |
5928 | break; | |
5929 | } | |
5930 | } | |
5931 | else { | |
5932 | // new part was added | |
5933 | $searchString = $part; | |
5934 | break; | |
5935 | } | |
5936 | } | |
5937 | ||
5938 | this._oldSearchString = $current; | |
5939 | } | |
5940 | ||
5941 | return $searchString; | |
5942 | }, | |
5943 | ||
4ad00d80 AE |
5944 | /** |
5945 | * Returns parameters for quick search. | |
5946 | * | |
5947 | * @param object parameters | |
5948 | * @return object | |
5949 | */ | |
5950 | _getParameters: function(parameters) { | |
5951 | return parameters; | |
5952 | }, | |
9f959ced | 5953 | |
046e1292 AE |
5954 | /** |
5955 | * Evalutes search results. | |
5956 | * | |
5957 | * @param object data | |
5958 | * @param string textStatus | |
5959 | * @param jQuery jqXHR | |
5960 | */ | |
5961 | _success: function(data, textStatus, jqXHR) { | |
7f45f320 | 5962 | this._clearList(false); |
b608ea34 | 5963 | this._searchInput.parents('.searchBar').removeClass('loading'); |
7f45f320 | 5964 | |
c138555b AE |
5965 | if ($.getLength(data.returnValues)) { |
5966 | for (var $i in data.returnValues) { | |
5967 | var $item = data.returnValues[$i]; | |
5968 | ||
5969 | this._createListItem($item); | |
5970 | } | |
046e1292 | 5971 | } |
c138555b AE |
5972 | else if (!this._handleEmptyResult()) { |
5973 | return; | |
046e1292 | 5974 | } |
4ad00d80 | 5975 | |
1af138b0 AE |
5976 | WCF.CloseOverlayHandler.addCallback('WCF.Search.Base', $.proxy(function() { this._clearList(); }, this)); |
5977 | ||
2d305fef AE |
5978 | var $containerID = this._searchInput.parents('.dropdown').wcfIdentify(); |
5979 | if (!WCF.Dropdown.getDropdownMenu($containerID).hasClass('dropdownOpen')) { | |
5980 | WCF.Dropdown.toggleDropdown($containerID); | |
114bb112 AE |
5981 | } |
5982 | ||
1af138b0 | 5983 | // pre-select first item |
4765dbfd | 5984 | this._itemIndex = -1; |
2d305fef AE |
5985 | if (!WCF.Dropdown.getDropdown($containerID).data('disableAutoFocus')) { |
5986 | this._selectNextItem(); | |
5987 | } | |
046e1292 | 5988 | }, |
4ad00d80 | 5989 | |
c138555b AE |
5990 | /** |
5991 | * Handles empty result lists, should return false if dropdown should be hidden. | |
5992 | * | |
5993 | * @return boolean | |
5994 | */ | |
5995 | _handleEmptyResult: function() { | |
5996 | return false; | |
5997 | }, | |
5998 | ||
4ad00d80 AE |
5999 | /** |
6000 | * Creates a new list item. | |
6001 | * | |
6002 | * @param object item | |
6003 | * @return jQuery | |
6004 | */ | |
6005 | _createListItem: function(item) { | |
83fccf43 | 6006 | var $listItem = $('<li><span>' + WCF.String.escapeHTML(item.label) + '</span></li>').appendTo(this._list); |
1c93e6e7 | 6007 | $listItem.data('objectID', item.objectID).data('label', item.label).click($.proxy(this._executeCallback, this)); |
4ad00d80 | 6008 | |
9a130ab4 AE |
6009 | this._itemCount++; |
6010 | ||
4ad00d80 AE |
6011 | return $listItem; |
6012 | }, | |
9f959ced | 6013 | |
046e1292 AE |
6014 | /** |
6015 | * Executes callback upon result click. | |
6016 | * | |
6017 | * @param object event | |
6018 | */ | |
6019 | _executeCallback: function(event) { | |
b0ce5298 | 6020 | var $clearSearchInput = false; |
046e1292 | 6021 | var $listItem = $(event.currentTarget); |
046e1292 | 6022 | // notify callback |
cf55c379 AE |
6023 | if (this._commaSeperated) { |
6024 | // auto-complete current part | |
6025 | var $result = $listItem.data('label'); | |
6026 | for (var $i = 0, $length = this._oldSearchString.length; $i < $length; $i++) { | |
6027 | var $part = this._oldSearchString[$i]; | |
47573ca9 | 6028 | if ($result.toLowerCase().indexOf($part.toLowerCase()) === 0) { |
cf55c379 | 6029 | this._oldSearchString[$i] = $result; |
cf1ca3a5 | 6030 | this._searchInput.val(this._oldSearchString.join(', ')); |
cf55c379 AE |
6031 | |
6032 | if ($.browser.webkit) { | |
6033 | // chrome won't display the new value until the textarea is rendered again | |
6034 | // this quick fix forces chrome to render it again, even though it changes nothing | |
6035 | this._searchInput.css({ display: 'block' }); | |
6036 | } | |
6037 | ||
68097635 | 6038 | // set focus on input field again |
47573ca9 | 6039 | var $position = this._searchInput.val().toLowerCase().indexOf($result.toLowerCase()) + $result.length; |
68097635 AE |
6040 | this._searchInput.focus().setCaret($position); |
6041 | ||
cf55c379 AE |
6042 | break; |
6043 | } | |
6044 | } | |
6045 | } | |
6046 | else { | |
b0ce5298 AE |
6047 | if (this._callback === null) { |
6048 | this._searchInput.val($listItem.data('label')); | |
6049 | } | |
6050 | else { | |
6051 | $clearSearchInput = (this._callback($listItem.data()) === true) ? true : false; | |
6052 | } | |
cf55c379 | 6053 | } |
9f959ced | 6054 | |
046e1292 | 6055 | // close list and revert input |
f28efd50 | 6056 | this._clearList($clearSearchInput); |
046e1292 | 6057 | }, |
9f959ced | 6058 | |
046e1292 AE |
6059 | /** |
6060 | * Closes the suggestion list and clears search input on demand. | |
6061 | * | |
6062 | * @param boolean clearSearchInput | |
6063 | */ | |
6064 | _clearList: function(clearSearchInput) { | |
cf55c379 | 6065 | if (clearSearchInput && !this._commaSeperated) { |
046e1292 AE |
6066 | this._searchInput.val(''); |
6067 | } | |
9f959ced | 6068 | |
596a5751 MS |
6069 | // close dropdown |
6070 | WCF.Dropdown.getDropdown(this._searchInput.parents('.dropdown').wcfIdentify()).removeClass('dropdownOpen'); | |
6071 | WCF.Dropdown.getDropdownMenu(this._searchInput.parents('.dropdown').wcfIdentify()).removeClass('dropdownOpen'); | |
6072 | ||
6073 | this._list.end().empty(); | |
7f45f320 AE |
6074 | |
6075 | WCF.CloseOverlayHandler.removeCallback('WCF.Search.Base'); | |
9a130ab4 AE |
6076 | |
6077 | // reset item navigation | |
6078 | this._itemCount = 0; | |
6079 | this._itemIndex = -1; | |
c000b08a MS |
6080 | }, |
6081 | ||
6082 | /** | |
6083 | * Adds an excluded search value. | |
6084 | * | |
6085 | * @param string value | |
6086 | */ | |
6087 | addExcludedSearchValue: function(value) { | |
6088 | if (!WCF.inArray(value, this._excludedSearchValues)) { | |
6089 | this._excludedSearchValues.push(value); | |
6090 | } | |
6091 | }, | |
6092 | ||
6093 | /** | |
0b17ce2f | 6094 | * Removes an excluded search value. |
c000b08a MS |
6095 | * |
6096 | * @param string value | |
6097 | */ | |
6098 | removeExcludedSearchValue: function(value) { | |
6099 | var index = $.inArray(value, this._excludedSearchValues); | |
6100 | if (index != -1) { | |
6101 | this._excludedSearchValues.splice(index, 1); | |
6102 | } | |
046e1292 AE |
6103 | } |
6104 | }); | |
6105 | ||
4ad00d80 AE |
6106 | /** |
6107 | * Provides quick search for users and user groups. | |
6108 | * | |
6109 | * @see WCF.Search.Base | |
6110 | */ | |
6111 | WCF.Search.User = WCF.Search.Base.extend({ | |
6112 | /** | |
6113 | * @see WCF.Search.Base._className | |
6114 | */ | |
6115 | _className: 'wcf\\data\\user\\UserAction', | |
6116 | ||
6117 | /** | |
6118 | * include user groups in search | |
6119 | * @var boolean | |
6120 | */ | |
6121 | _includeUserGroups: false, | |
6122 | ||
6123 | /** | |
cf55c379 | 6124 | * @see WCF.Search.Base.init() |
4ad00d80 | 6125 | */ |
cf55c379 | 6126 | init: function(searchInput, callback, includeUserGroups, excludedSearchValues, commaSeperated) { |
4ad00d80 AE |
6127 | this._includeUserGroups = includeUserGroups; |
6128 | ||
cf55c379 | 6129 | this._super(searchInput, callback, excludedSearchValues, commaSeperated); |
4ad00d80 AE |
6130 | }, |
6131 | ||
6132 | /** | |
6133 | * @see WCF.Search.Base._getParameters() | |
6134 | */ | |
6135 | _getParameters: function(parameters) { | |
4ccbb0bb | 6136 | parameters.data.includeUserGroups = this._includeUserGroups ? 1 : 0; |
1c93e6e7 AE |
6137 | |
6138 | return parameters; | |
4ad00d80 AE |
6139 | }, |
6140 | ||
6141 | /** | |
6142 | * @see WCF.Search.Base._createListItem() | |
6143 | */ | |
6144 | _createListItem: function(item) { | |
6145 | var $listItem = this._super(item); | |
6146 | ||
c2d0b2d6 MS |
6147 | var $icon = null; |
6148 | if (item.icon) { | |
6149 | $icon = $(item.icon); | |
6150 | } | |
6151 | else if (this._includeUserGroups && item.type === 'group') { | |
6152 | $icon = $('<span class="icon icon16 icon-group" />'); | |
6153 | } | |
6154 | ||
6155 | if ($icon) { | |
6156 | var $label = $listItem.find('span').detach(); | |
6157 | ||
6158 | var $box16 = $('<div />').addClass('box16').appendTo($listItem); | |
6159 | ||
32811021 | 6160 | $box16.append($icon); |
c2d0b2d6 MS |
6161 | $box16.append($('<div />').append($label)); |
6162 | } | |
6163 | ||
4ad00d80 | 6164 | // insert item type |
4ad00d80 AE |
6165 | $listItem.data('type', item.type); |
6166 | ||
6167 | return $listItem; | |
6168 | } | |
6169 | }); | |
6170 | ||
5568e009 AE |
6171 | /** |
6172 | * Namespace for system-related classes. | |
6173 | */ | |
6174 | WCF.System = { }; | |
6175 | ||
eb1537e3 AE |
6176 | /** |
6177 | * Namespace for dependency-related classes. | |
6178 | */ | |
6179 | WCF.System.Dependency = { }; | |
6180 | ||
6181 | /** | |
6182 | * JavaScript Dependency Manager. | |
6183 | */ | |
6184 | WCF.System.Dependency.Manager = { | |
6185 | /** | |
6186 | * list of callbacks grouped by identifier | |
6187 | * @var object | |
6188 | */ | |
6189 | _callbacks: { }, | |
6190 | ||
6191 | /** | |
6192 | * list of loaded identifiers | |
6193 | * @var array<string> | |
6194 | */ | |
6195 | _loaded: [ ], | |
6196 | ||
6197 | /** | |
6198 | * list of setup callbacks grouped by identifier | |
6199 | * @var object | |
6200 | */ | |
6201 | _setupCallbacks: { }, | |
6202 | ||
6203 | /** | |
6204 | * Registers a callback for given identifier, will be executed after all setup | |
6205 | * callbacks have been invoked. | |
6206 | * | |
6207 | * @param string identifier | |
6208 | * @param object callback | |
6209 | */ | |
6210 | register: function(identifier, callback) { | |
6211 | if (!$.isFunction(callback)) { | |
6212 | console.debug("[WCF.System.Dependency.Manager] Callback for identifier '" + identifier + "' is invalid, aborting."); | |
6213 | return; | |
6214 | } | |
6215 | ||
6216 | // already loaded, invoke now | |
6217 | if (WCF.inArray(identifier, this._loaded)) { | |
9fb5eeac AE |
6218 | setTimeout(function() { |
6219 | callback(); | |
6220 | }, 1); | |
eb1537e3 AE |
6221 | } |
6222 | else { | |
6223 | if (!this._callbacks[identifier]) { | |
6224 | this._callbacks[identifier] = [ ]; | |
6225 | } | |
6226 | ||
6227 | this._callbacks[identifier].push(callback); | |
6228 | } | |
6229 | }, | |
6230 | ||
6231 | /** | |
6232 | * Registers a setup callback for given identifier, will be invoked | |
6233 | * prior to all other callbacks. | |
6234 | * | |
6235 | * @param string identifier | |
6236 | * @param object callback | |
6237 | */ | |
6238 | setup: function(identifier, callback) { | |
6239 | if (!$.isFunction(callback)) { | |
6240 | console.debug("[WCF.System.Dependency.Manager] Setup callback for identifier '" + identifier + "' is invalid, aborting."); | |
6241 | return; | |
6242 | } | |
6243 | ||
6244 | if (!this._setupCallbacks[identifier]) { | |
6245 | this._setupCallbacks[identifier] = [ ]; | |
6246 | } | |
6247 | ||
6248 | this._setupCallbacks[identifier].push(callback); | |
6249 | }, | |
6250 | ||
6251 | /** | |
6252 | * Invokes all callbacks for given identifier and marks it as loaded. | |
6253 | * | |
6254 | * @param string identifier | |
6255 | */ | |
6256 | invoke: function(identifier) { | |
6257 | if (this._setupCallbacks[identifier]) { | |
6258 | for (var $i = 0, $length = this._setupCallbacks[identifier].length; $i < $length; $i++) { | |
6259 | this._setupCallbacks[identifier][$i](); | |
6260 | } | |
2f51337e AE |
6261 | |
6262 | delete this._setupCallbacks[identifier]; | |
eb1537e3 AE |
6263 | } |
6264 | ||
6265 | this._loaded.push(identifier); | |
6266 | ||
6267 | if (this._callbacks[identifier]) { | |
6268 | for (var $i = 0, $length = this._callbacks[identifier].length; $i < $length; $i++) { | |
6269 | this._callbacks[identifier][$i](); | |
6270 | } | |
2f51337e AE |
6271 | |
6272 | delete this._callbacks[identifier]; | |
eb1537e3 AE |
6273 | } |
6274 | } | |
6275 | }; | |
6276 | ||
88c8e868 | 6277 | /** |
7b608580 | 6278 | * Provides flexible dropdowns for tab-based menus. |
88c8e868 | 6279 | */ |
7b608580 AE |
6280 | WCF.System.FlexibleMenu = { |
6281 | /** | |
6282 | * list of containers | |
6283 | * @var object<jQuery> | |
6284 | */ | |
6285 | _containers: { }, | |
6286 | ||
6287 | /** | |
6288 | * list of registered container ids | |
6289 | * @var array<string> | |
6290 | */ | |
6291 | _containerIDs: [ ], | |
6292 | ||
6293 | /** | |
6294 | * list of dropdowns | |
6295 | * @var object<jQuery> | |
6296 | */ | |
6297 | _dropdowns: { }, | |
6298 | ||
6299 | /** | |
6300 | * list of dropdown menus | |
6301 | * @var object<jQuery> | |
6302 | */ | |
6303 | _dropdownMenus: { }, | |
6304 | ||
6305 | /** | |
6306 | * list of hidden status for containers | |
6307 | * @var object<boolean> | |
6308 | */ | |
6309 | _hasHiddenItems: { }, | |
6310 | ||
6311 | /** | |
6312 | * true if menus are currently rebuilt | |
6313 | * @var boolean | |
6314 | */ | |
6315 | _isWorking: false, | |
6316 | ||
6317 | /** | |
6318 | * list of tab menu items per container | |
6319 | * @var object<jQuery> | |
6320 | */ | |
6321 | _menuItems: { }, | |
6322 | ||
6323 | /** | |
6324 | * Initializes the WCF.System.FlexibleMenu class. | |
6325 | */ | |
88c8e868 | 6326 | init: function() { |
7b608580 AE |
6327 | // register .mainMenu and .navigationHeader by default |
6328 | this.registerMenu('mainMenu'); | |
6329 | this.registerMenu($('.navigationHeader:eq(0)').wcfIdentify()); | |
88c8e868 | 6330 | |
8e514e3f AE |
6331 | this._registerTabMenus(); |
6332 | ||
7b608580 | 6333 | $(window).resize($.proxy(this.rebuildAll, this)); |
8e514e3f AE |
6334 | WCF.DOMNodeInsertedHandler.addCallback('WCF.System.FlexibleMenu', $.proxy(this._registerTabMenus, this)); |
6335 | }, | |
6336 | ||
6337 | /** | |
6338 | * Registers tab menus. | |
6339 | */ | |
6340 | _registerTabMenus: function() { | |
6341 | // register tab menus | |
6342 | $('.tabMenuContainer:not(.jsFlexibleMenuEnabled)').each(function(index, tabMenuContainer) { | |
6343 | var $navigation = $(tabMenuContainer).children('nav'); | |
6344 | if ($navigation.length && $navigation.find('> ul:eq(0) > li').length) { | |
6345 | WCF.System.FlexibleMenu.registerMenu($navigation.wcfIdentify()); | |
6346 | } | |
6347 | }); | |
88c8e868 MW |
6348 | }, |
6349 | ||
7b608580 AE |
6350 | /** |
6351 | * Registers a tab-based menu by id. | |
6352 | * | |
6353 | * Required DOM: | |
6354 | * <container> | |
6355 | * <ul style="white-space: nowrap"> | |
6356 | * <li>tab 1</li> | |
6357 | * <li>tab 2</li> | |
6358 | * ... | |
6359 | * <li>tab n</li> | |
6360 | * </ul> | |
6361 | * </container> | |
6362 | * | |
6363 | * @param string containerID | |
6364 | */ | |
6365 | registerMenu: function(containerID) { | |
6366 | var $container = $('#' + containerID); | |
6367 | if (!$container.length) { | |
6368 | console.debug("[WCF.System.FlexibleMenu] Unable to find container identified by '" + containerID + "', aborting."); | |
6369 | return; | |
6370 | } | |
6371 | ||
6372 | this._containerIDs.push(containerID); | |
6373 | this._containers[containerID] = $container; | |
6374 | this._menuItems[containerID] = $container.find('> ul:eq(0) > li'); | |
67fde3bf | 6375 | this._dropdowns[containerID] = $('<li class="dropdown"><a class="icon icon16 icon-list" /></li>').data('containerID', containerID).click($.proxy(this._click, this)); |
7b608580 AE |
6376 | this._dropdownMenus[containerID] = $('<ul class="dropdownMenu" />').appendTo(this._dropdowns[containerID]); |
6377 | this._hasHiddenItems[containerID] = false; | |
6378 | ||
6379 | this.rebuild(containerID); | |
6380 | ||
6381 | WCF.Dropdown.initDropdown(this._dropdowns[containerID].children('a')); | |
6382 | }, | |
6383 | ||
6384 | /** | |
6385 | * Rebuilds all registered containers. | |
6386 | */ | |
6387 | rebuildAll: function() { | |
6388 | if (this._isWorking) { | |
6389 | return; | |
6390 | } | |
6391 | ||
6392 | this._isWorking = true; | |
6393 | ||
6394 | for (var $i = 0, $length = this._containerIDs.length; $i < $length; $i++) { | |
6395 | this.rebuild(this._containerIDs[$i]); | |
6396 | } | |
6397 | ||
6398 | this._isWorking = false; | |
6399 | }, | |
6400 | ||
6401 | /** | |
6402 | * Rebuilds a container, will be automatically invoked on window resize and registering. | |
6403 | * | |
6404 | * @param string containerID | |
6405 | */ | |
6406 | rebuild: function(containerID) { | |
6407 | if (!this._containers[containerID]) { | |
6408 | console.debug("[WCF.System.FlexibleMenu] Cannot rebuild unknown container identified by '" + containerID + "'"); | |
6409 | return; | |
6410 | } | |
6411 | ||
6412 | var $changedItems = false; | |
377a49cc | 6413 | var $container = this._containers[containerID]; |
7b608580 AE |
6414 | var $currentWidth = 0; |
6415 | ||
6416 | // the current width is based upon all items without the dropdown | |
6417 | var $menuItems = this._menuItems[containerID].filter(':visible'); | |
6418 | for (var $i = 0, $length = $menuItems.length; $i < $length; $i++) { | |
6419 | $currentWidth += $($menuItems[$i]).outerWidth(true); | |
6420 | } | |
6421 | ||
377a49cc AE |
6422 | // insert dropdown for calculation purposes |
6423 | if (!this._hasHiddenItems[containerID]) { | |
6424 | this._dropdowns[containerID].appendTo($container.children('ul:eq(0)')); | |
6425 | } | |
6426 | ||
7b608580 | 6427 | var $dropdownWidth = this._dropdowns[containerID].outerWidth(true); |
377a49cc AE |
6428 | |
6429 | // remove dropdown previously inserted | |
6430 | if (!this._hasHiddenItems[containerID]) { | |
6431 | this._dropdowns[containerID].detach(); | |
6432 | } | |
6433 | ||
8960a235 | 6434 | var $maximumWidth = $container.parent().innerWidth(); |
7b608580 AE |
6435 | |
6436 | // substract padding from the parent element | |
6437 | $maximumWidth -= parseInt($container.parent().css('padding-left').replace(/px$/, '')) + parseInt($container.parent().css('padding-right').replace(/px$/, '')); | |
6438 | ||
6439 | // substract margins and paddings from the container itself | |
6440 | $maximumWidth -= parseInt($container.css('margin-left').replace(/px$/, '')) + parseInt($container.css('margin-right').replace(/px$/, '')); | |
6441 | $maximumWidth -= parseInt($container.css('padding-left').replace(/px$/, '')) + parseInt($container.css('padding-right').replace(/px$/, '')); | |
6442 | ||
6443 | // substract paddings from the actual list | |
6444 | $maximumWidth -= parseInt($container.children('ul:eq(0)').css('padding-left').replace(/px$/, '')) + parseInt($container.children('ul:eq(0)').css('padding-right').replace(/px$/, '')); | |
8960a235 AE |
6445 | if ($currentWidth > $maximumWidth || (this._hasHiddenItems[containerID] && ($currentWidth > $maximumWidth - $dropdownWidth))) { |
6446 | var $menuItems = $menuItems.filter(':not(.active):not(.ui-state-active):visible'); | |
6447 | ||
6448 | // substract dropdown width from maximum width | |
6449 | $maximumWidth -= $dropdownWidth; | |
7b608580 AE |
6450 | |
6451 | // hide items starting with the last in list (ignores active item) | |
6452 | for (var $i = ($menuItems.length - 1); $i >= 0; $i--) { | |
6453 | if ($currentWidth > $maximumWidth) { | |
6454 | var $item = $($menuItems[$i]); | |
6455 | $currentWidth -= $item.outerWidth(true); | |
6456 | $item.hide(); | |
6457 | ||
6458 | $changedItems = true; | |
6459 | this._hasHiddenItems[containerID] = true; | |
6460 | } | |
6461 | else { | |
6462 | break; | |
6463 | } | |
6464 | } | |
6465 | ||
6466 | if (this._hasHiddenItems[containerID]) { | |
67fde3bf | 6467 | this._dropdowns[containerID].appendTo($container.children('ul:eq(0)')); |
7b608580 AE |
6468 | } |
6469 | } | |
6470 | else if (this._hasHiddenItems[containerID] && $currentWidth < $maximumWidth) { | |
6471 | var $hiddenItems = this._menuItems[containerID].filter(':not(:visible)'); | |
6472 | ||
8960a235 AE |
6473 | // substract dropdown width from maximum width unless it is the last item |
6474 | $maximumWidth -= $dropdownWidth; | |
6475 | ||
7b608580 AE |
6476 | // reverts items starting with the first hidden one |
6477 | for (var $i = 0, $length = $hiddenItems.length; $i < $length; $i++) { | |
6478 | var $item = $($hiddenItems[$i]); | |
6479 | $currentWidth += $item.outerWidth(); | |
8960a235 AE |
6480 | |
6481 | if ($i + 1 == $length) { | |
6482 | $maximumWidth += $dropdownWidth; | |
6483 | } | |
6484 | ||
7b608580 AE |
6485 | if ($currentWidth < $maximumWidth) { |
6486 | // enough space, show item | |
6487 | $item.css('display', ''); | |
6488 | $changedItems = true; | |
6489 | } | |
6490 | else { | |
6491 | break; | |
6492 | } | |
6493 | } | |
6494 | ||
6495 | if ($changedItems) { | |
6496 | this._hasHiddenItems[containerID] = (this._menuItems[containerID].filter(':not(:visible)').length > 0); | |
6497 | if (!this._hasHiddenItems[containerID]) { | |
67fde3bf | 6498 | this._dropdowns[containerID].detach(); |
7b608580 AE |
6499 | } |
6500 | } | |
6501 | } | |
6502 | ||
6503 | // build dropdown menu for hidden items | |
6504 | if ($changedItems) { | |
6505 | this._dropdownMenus[containerID].empty(); | |
6506 | this._menuItems[containerID].filter(':not(:visible)').each($.proxy(function(index, item) { | |
6507 | $('<li>' + $(item).html() + '</li>').appendTo(this._dropdownMenus[containerID]); | |
6508 | }, this)); | |
6509 | } | |
88c8e868 MW |
6510 | } |
6511 | }; | |
6512 | ||
23192d23 AE |
6513 | /** |
6514 | * Namespace for mobile device-related classes. | |
6515 | */ | |
6516 | WCF.System.Mobile = { }; | |
6517 | ||
6518 | /** | |
6519 | * Handles general navigation and UX on mobile devices. | |
6520 | */ | |
6521 | WCF.System.Mobile.UX = { | |
41385e13 AE |
6522 | /** |
6523 | * true if mobile optimizations are enabled | |
6524 | * @var boolean | |
6525 | */ | |
6526 | _enabled: false, | |
6527 | ||
23192d23 AE |
6528 | /** |
6529 | * main container | |
6530 | * @var jQuery | |
6531 | */ | |
6532 | _main: null, | |
6533 | ||
41385e13 AE |
6534 | /** |
6535 | * sidebar container | |
6536 | * @var jQuery | |
6537 | */ | |
6538 | _sidebar: null, | |
6539 | ||
23192d23 AE |
6540 | /** |
6541 | * Initializes the WCF.System.Mobile.UX class. | |
6542 | */ | |
6543 | init: function() { | |
41385e13 | 6544 | this._enabled = false; |
23192d23 | 6545 | this._main = $('#main'); |
41385e13 | 6546 | this._sidebar = this._main.find('> div > div > .sidebar'); |
23192d23 | 6547 | |
41385e13 AE |
6548 | if ($.browser.touch) { |
6549 | $('html').addClass('touch'); | |
6550 | } | |
6551 | ||
6552 | enquire.register('screen and (max-width: 800px)', { | |
6553 | match: $.proxy(this._enable, this), | |
6554 | unmatch: $.proxy(this._disable, this), | |
6555 | setup: $.proxy(this._setup, this), | |
6556 | deferSetup: true | |
6557 | }); | |
23192d23 | 6558 | |
41385e13 AE |
6559 | if ($.browser.msie && this._sidebar.width() > 305) { |
6560 | // sidebar is rarely broken on IE9/IE10 | |
6561 | this._sidebar.css('display', 'none').css('display', ''); | |
6562 | } | |
6563 | }, | |
6564 | ||
6565 | /** | |
6566 | * Initializes the mobile optimization once the media query matches. | |
6567 | */ | |
6568 | _setup: function() { | |
23192d23 AE |
6569 | this._initSidebarToggleButtons(); |
6570 | this._initSearchBar(); | |
6571 | this._initButtonGroupNavigation(); | |
6572 | ||
f3e114a1 | 6573 | WCF.CloseOverlayHandler.addCallback('WCF.System.Mobile.UX', $.proxy(this._closeMenus, this)); |
23192d23 AE |
6574 | WCF.DOMNodeInsertedHandler.addCallback('WCF.System.Mobile.UX', $.proxy(this._initButtonGroupNavigation, this)); |
6575 | }, | |
6576 | ||
41385e13 AE |
6577 | /** |
6578 | * Enables the mobile optimization. | |
6579 | */ | |
6580 | _enable: function() { | |
6581 | this._enabled = true; | |
6582 | ||
6583 | if ($.browser.msie) { | |
6584 | this._sidebar.css('display', 'none').css('display', ''); | |
6585 | } | |
6586 | }, | |
6587 | ||
6588 | /** | |
6589 | * Disables the mobile optimization. | |
6590 | */ | |
6591 | _disable: function() { | |
6592 | this._enabled = false; | |
6593 | ||
6594 | if ($.browser.msie) { | |
6595 | this._sidebar.css('display', 'none').css('display', ''); | |
6596 | } | |
6597 | }, | |
6598 | ||
23192d23 AE |
6599 | /** |
6600 | * Initializes the sidebar toggle buttons. | |
6601 | */ | |
6602 | _initSidebarToggleButtons: function() { | |
6603 | var $sidebarLeft = this._main.hasClass('sidebarOrientationLeft'); | |
6604 | var $sidebarRight = this._main.hasClass('sidebarOrientationRight'); | |
6605 | if ($sidebarLeft || $sidebarRight) { | |
6606 | // use icons if language item is empty/non-existant | |
6607 | var $languageShowSidebar = 'wcf.global.sidebar.show' + ($sidebarLeft ? 'Left' : 'Right') + 'Sidebar'; | |
6608 | if ($languageShowSidebar === WCF.Language.get($languageShowSidebar) || WCF.Language.get($languageShowSidebar) === '') { | |
6609 | $languageShowSidebar = '<span class="icon icon16 icon-double-angle-' + ($sidebarLeft ? 'left' : 'right') + '" />'; | |
6610 | } | |
6611 | ||
6612 | var $languageHideSidebar = 'wcf.global.sidebar.hide' + ($sidebarLeft ? 'Left' : 'Right') + 'Sidebar'; | |
6613 | if ($languageHideSidebar === WCF.Language.get($languageHideSidebar) || WCF.Language.get($languageHideSidebar) === '') { | |
6614 | $languageHideSidebar = '<span class="icon icon16 icon-double-angle-' + ($sidebarLeft ? 'right' : 'left') + '" />'; | |
6615 | } | |
6616 | ||
6617 | // add toggle buttons | |
6618 | var self = this; | |
6619 | $('<span class="button small mobileSidebarToggleButton">' + $languageShowSidebar + '</span>').appendTo($('.content')).click(function() { self._main.addClass('mobileShowSidebar'); }); | |
6620 | $('<span class="button small mobileSidebarToggleButton">' + $languageHideSidebar + '</span>').appendTo($('.sidebar')).click(function() { self._main.removeClass('mobileShowSidebar'); }); | |
6621 | } | |
6622 | }, | |
6623 | ||
6624 | /** | |
6625 | * Initializes the search bar. | |
6626 | */ | |
6627 | _initSearchBar: function() { | |
6628 | var $searchBar = $('.searchBar:eq(0)'); | |
6629 | ||
41385e13 AE |
6630 | var self = this; |
6631 | $searchBar.click(function() { | |
6632 | if (self._enabled) { | |
6633 | $searchBar.addClass('searchBarOpen'); | |
6634 | } | |
6635 | }); | |
6636 | ||
23192d23 AE |
6637 | this._main.click(function() { $searchBar.removeClass('searchBarOpen'); }); |
6638 | }, | |
6639 | ||
6640 | /** | |
6641 | * Initializes the button group lists, converting them into native dropdowns. | |
6642 | */ | |
6643 | _initButtonGroupNavigation: function() { | |
6644 | $('.buttonGroupNavigation:not(.jsMobileButtonGroupNavigation)').each(function(index, navigation) { | |
41385e13 | 6645 | var $navigation = $(navigation).addClass('jsMobileButtonGroupNavigation'); |
23192d23 AE |
6646 | var $button = $('<a class="dropdownLabel"><span class="icon icon24 icon-list" /></a>').prependTo($navigation); |
6647 | ||
f3e114a1 | 6648 | $button.click(function() { $button.next().toggleClass('open'); return false; }); |
23192d23 | 6649 | }); |
f3e114a1 AE |
6650 | }, |
6651 | ||
6652 | /** | |
6653 | * Closes menus. | |
6654 | */ | |
6655 | _closeMenus: function() { | |
6656 | $('.jsMobileButtonGroupNavigation > ul.open').removeClass('open'); | |
23192d23 AE |
6657 | } |
6658 | }; | |
6659 | ||
eb0f6246 AE |
6660 | WCF.System.Page = { }; |
6661 | ||
298a7cd8 AE |
6662 | WCF.System.Page.Multiple = Class.extend({ |
6663 | _cache: { }, | |
6664 | _options: { }, | |
6665 | _pageNo: 1, | |
6666 | _pages: 0, | |
66ed9925 | 6667 | _previousPageNo: 0, |
298a7cd8 AE |
6668 | |
6669 | init: function(options) { | |
6670 | this._options = $.extend({ | |
6671 | // elements | |
6672 | container: null, | |
6673 | pagination: null, | |
6674 | ||
6675 | // callbacks | |
6676 | loadItems: null | |
6677 | }, options); | |
66ed9925 AE |
6678 | |
6679 | this._cache = { }; | |
6680 | this._pageNo = 1; | |
6681 | this._pages = 0; | |
6682 | this._previousPageNo = 0; | |
6683 | ||
6684 | if (this._pagination.data('pages')) { | |
6685 | this._pagination.wcfPages({ | |
6686 | maxPage: this._pagination.data('pages') | |
6687 | }).on('wcfpagesswitched', $.proxy(this._showPage, this)); | |
6688 | } | |
298a7cd8 AE |
6689 | }, |
6690 | ||
6691 | /** | |
6692 | * Callback after page has changed. | |
6693 | * | |
6694 | * @param object event | |
6695 | * @param object data | |
6696 | */ | |
6697 | _showPage: function(event, data) { | |
6698 | if (data && data.activePage) { | |
6699 | if (!data.template) { | |
6700 | this._previousPageNo = this._pageNo; | |
6701 | } | |
6702 | ||
6703 | this._pageNo = data.activePage; | |
6704 | } | |
6705 | ||
6706 | if (this._cache[this._pageNo] || (data && data.template)) { | |
6707 | this._cache[this._previousPageNo] = this._list.children().detach(); | |
6708 | ||
6709 | if (data && data.template) { | |
6710 | this._list.html(data.template); | |
6711 | } | |
6712 | else { | |
6713 | this._list.append(this._cache[this._pageNo]); | |
6714 | } | |
6715 | } | |
6716 | else { | |
66ed9925 | 6717 | this._options.loadItems(); |
298a7cd8 AE |
6718 | } |
6719 | }, | |
6720 | ||
6721 | showPage: function(pageNo, template) { | |
6722 | this._showPage(null, { | |
6723 | activePage: pageNo, | |
6724 | template: template | |
6725 | }); | |
66ed9925 AE |
6726 | }, |
6727 | ||
6728 | getPageNo: function() { | |
6729 | return this._pageNo; | |
298a7cd8 AE |
6730 | } |
6731 | }); | |
6732 | ||
5568e009 AE |
6733 | /** |
6734 | * System notification overlays. | |
6735 | * | |
6736 | * @param string message | |
6737 | * @param string cssClassNames | |
6738 | */ | |
6739 | WCF.System.Notification = Class.extend({ | |
6740 | /** | |
6741 | * callback on notification close | |
6742 | * @var object | |
6743 | */ | |
6744 | _callback: null, | |
6745 | ||
d2e126c6 AE |
6746 | /** |
6747 | * CSS class names | |
6748 | * @var string | |
6749 | */ | |
6750 | _cssClassNames: '', | |
6751 | ||
6752 | /** | |
6753 | * notification message | |
6754 | * @var string | |
6755 | */ | |
6756 | _message: '', | |
6757 | ||
5568e009 AE |
6758 | /** |
6759 | * notification overlay | |
6760 | * @var jQuery | |
6761 | */ | |
6762 | _overlay: null, | |
6763 | ||
6764 | /** | |
6765 | * Creates a new system notification overlay. | |
6766 | * | |
6767 | * @param string message | |
6768 | * @param string cssClassNames | |
6769 | */ | |
6770 | init: function(message, cssClassNames) { | |
d2e126c6 | 6771 | this._cssClassNames = cssClassNames || 'success'; |
11cf19be | 6772 | this._message = message || WCF.Language.get('wcf.global.success'); |
7c1fa02a | 6773 | this._overlay = $('#systemNotification'); |
5568e009 | 6774 | |
d2e126c6 | 6775 | if (!this._overlay.length) { |
7ba6d79a | 6776 | this._overlay = $('<div id="systemNotification"><p></p></div>').hide().appendTo(document.body); |
5568e009 AE |
6777 | } |
6778 | }, | |
6779 | ||
6780 | /** | |
6781 | * Shows the notification overlay. | |
6782 | * | |
6783 | * @param object callback | |
6784 | * @param integer duration | |
7c1fa02a AE |
6785 | * @param string message |
6786 | * @param string cssClassName | |
5568e009 | 6787 | */ |
7c1fa02a | 6788 | show: function(callback, duration, message, cssClassNames) { |
5568e009 AE |
6789 | duration = parseInt(duration); |
6790 | if (!duration) duration = 2000; | |
6791 | ||
6792 | if (callback && $.isFunction(callback)) { | |
6793 | this._callback = callback; | |
6794 | } | |
6795 | ||
d2e126c6 AE |
6796 | this._overlay.children('p').html((message || this._message)); |
6797 | this._overlay.children('p').removeClass().addClass((cssClassNames || this._cssClassNames)); | |
7c1fa02a | 6798 | |
5568e009 AE |
6799 | // hide overlay after specified duration |
6800 | new WCF.PeriodicalExecuter($.proxy(this._hide, this), duration); | |
6801 | ||
7ba6d79a | 6802 | this._overlay.wcfFadeIn(undefined, 300); |
5568e009 AE |
6803 | }, |
6804 | ||
6805 | /** | |
6806 | * Hides the notification overlay after executing the callback. | |
6807 | * | |
6808 | * @param WCF.PeriodicalExecuter pe | |
6809 | */ | |
6810 | _hide: function(pe) { | |
6811 | if (this._callback !== null) { | |
6812 | this._callback(); | |
6813 | } | |
6814 | ||
7ba6d79a | 6815 | this._overlay.wcfFadeOut(undefined, 300); |
5568e009 AE |
6816 | |
6817 | pe.stop(); | |
6818 | } | |
6819 | }); | |
6820 | ||
34e158d9 AE |
6821 | /** |
6822 | * Provides dialog-based confirmations. | |
6823 | */ | |
6824 | WCF.System.Confirmation = { | |
6825 | /** | |
6826 | * notification callback | |
6827 | * @var object | |
6828 | */ | |
6829 | _callback: null, | |
6830 | ||
6831 | /** | |
6832 | * confirmation dialog | |
6833 | * @var jQuery | |
6834 | */ | |
6835 | _dialog: null, | |
6836 | ||
f02b3f75 AE |
6837 | /** |
6838 | * callback parameters | |
6839 | * @var object | |
6840 | */ | |
6841 | _parameters: null, | |
6842 | ||
34e158d9 AE |
6843 | /** |
6844 | * dialog visibility | |
6845 | * @var boolean | |
6846 | */ | |
6847 | _visible: false, | |
6848 | ||
d36a62dc MK |
6849 | /** |
6850 | * confirmation button | |
6851 | * @var jQuery | |
6852 | */ | |
6853 | _confirmationButton: null, | |
6854 | ||
34e158d9 AE |
6855 | /** |
6856 | * Displays a confirmation dialog. | |
6857 | * | |
6858 | * @param string message | |
6859 | * @param object callback | |
f02b3f75 | 6860 | * @param object parameters |
73b84427 | 6861 | * @param jQuery template |
34e158d9 | 6862 | */ |
73b84427 | 6863 | show: function(message, callback, parameters, template) { |
34e158d9 AE |
6864 | if (this._visible) { |
6865 | console.debug('[WCF.System.Confirmation] Confirmation dialog is already open, refusing action.'); | |
6866 | return; | |
6867 | } | |
6868 | ||
6869 | if (!$.isFunction(callback)) { | |
6870 | console.debug('[WCF.System.Confirmation] Given callback is invalid, aborting.'); | |
6871 | return; | |
6872 | } | |
6873 | ||
6874 | this._callback = callback; | |
f02b3f75 | 6875 | this._parameters = parameters; |
e730016d AE |
6876 | |
6877 | var $render = true; | |
34e158d9 AE |
6878 | if (this._dialog === null) { |
6879 | this._createDialog(); | |
e730016d | 6880 | $render = false; |
34e158d9 AE |
6881 | } |
6882 | ||
73b84427 AE |
6883 | this._dialog.find('#wcfSystemConfirmationContent').empty().hide(); |
6884 | if (template && template.length) { | |
6885 | template.appendTo(this._dialog.find('#wcfSystemConfirmationContent').show()); | |
6886 | } | |
6887 | ||
7b0937e3 | 6888 | this._dialog.find('p').text(message); |
34e158d9 AE |
6889 | this._dialog.wcfDialog({ |
6890 | onClose: $.proxy(this._close, this), | |
6891 | onShow: $.proxy(this._show, this), | |
6892 | title: WCF.Language.get('wcf.global.confirmation.title') | |
6893 | }); | |
e730016d AE |
6894 | if ($render) { |
6895 | this._dialog.wcfDialog('render'); | |
6896 | } | |
34e158d9 | 6897 | |
d36a62dc | 6898 | this._confirmationButton.focus(); |
34e158d9 AE |
6899 | this._visible = true; |
6900 | }, | |
6901 | ||
6902 | /** | |
6903 | * Creates the confirmation dialog on first use. | |
6904 | */ | |
6905 | _createDialog: function() { | |
73b84427 | 6906 | this._dialog = $('<div id="wcfSystemConfirmation" class="systemConfirmation"><p /><div id="wcfSystemConfirmationContent" /></div>').hide().appendTo(document.body); |
582a1874 | 6907 | var $formButtons = $('<div class="formSubmit" />').appendTo(this._dialog); |
34e158d9 | 6908 | |
d36a62dc | 6909 | this._confirmationButton = $('<button class="buttonPrimary">' + WCF.Language.get('wcf.global.confirmation.confirm') + '</button>').data('action', 'confirm').click($.proxy(this._click, this)).appendTo($formButtons); |
34e158d9 AE |
6910 | $('<button>' + WCF.Language.get('wcf.global.confirmation.cancel') + '</button>').data('action', 'cancel').click($.proxy(this._click, this)).appendTo($formButtons); |
6911 | }, | |
6912 | ||
6913 | /** | |
6914 | * Handles button clicks. | |
6915 | * | |
6916 | * @param object event | |
6917 | */ | |
6918 | _click: function(event) { | |
6919 | this._notify($(event.currentTarget).data('action')); | |
6920 | }, | |
6921 | ||
6922 | /** | |
6923 | * Handles dialog being closed. | |
6924 | */ | |
6925 | _close: function() { | |
6926 | if (this._visible) { | |
6927 | this._notify('cancel'); | |
6928 | } | |
6929 | }, | |
6930 | ||
6931 | /** | |
6932 | * Notifies callback upon user's decision. | |
6933 | * | |
6934 | * @param string action | |
6935 | */ | |
6936 | _notify: function(action) { | |
6937 | this._visible = false; | |
6938 | this._dialog.wcfDialog('close'); | |
6939 | ||
f02b3f75 | 6940 | this._callback(action, this._parameters); |
34e158d9 AE |
6941 | }, |
6942 | ||
6943 | /** | |
6944 | * Tries to set focus on confirm button. | |
6945 | */ | |
6946 | _show: function() { | |
582a1874 | 6947 | this._dialog.find('button.buttonPrimary').blur().focus(); |
34e158d9 AE |
6948 | } |
6949 | }; | |
6950 | ||
b2918c9d TD |
6951 | /** |
6952 | * Disables the ability to scroll the page. | |
6953 | */ | |
6954 | WCF.System.DisableScrolling = { | |
6955 | /** | |
6956 | * number of times scrolling was disabled (nested calls) | |
6957 | * @var integer | |
6958 | */ | |
6959 | _depth: 0, | |
6960 | ||
6961 | /** | |
6962 | * old overflow-value of the body element | |
6963 | * @var string | |
6964 | */ | |
6965 | _oldOverflow: null, | |
6966 | ||
6967 | /** | |
6968 | * Disables scrolling. | |
6969 | */ | |
6970 | disable: function () { | |
1dd0c00d AE |
6971 | // do not block scrolling on touch devices |
6972 | if ($.browser.touch) { | |
6973 | return; | |
6974 | } | |
6975 | ||
b2918c9d TD |
6976 | if (this._depth === 0) { |
6977 | this._oldOverflow = $(document.body).css('overflow'); | |
6978 | $(document.body).css('overflow', 'hidden'); | |
6979 | } | |
6980 | ||
6981 | this._depth++; | |
6982 | }, | |
6983 | ||
6984 | /** | |
6985 | * Enables scrolling again. | |
6986 | * Must be called the same number of times disable() was called to enable scrolling. | |
6987 | */ | |
6988 | enable: function () { | |
6989 | if (this._depth === 0) return; | |
6990 | ||
6991 | this._depth--; | |
6992 | ||
6993 | if (this._depth === 0) { | |
6994 | $(document.body).css('overflow', this._oldOverflow); | |
6995 | } | |
6996 | } | |
6997 | }; | |
6998 | ||
d83e246c AE |
6999 | /** |
7000 | * Provides the 'jump to page' overlay. | |
7001 | */ | |
7002 | WCF.System.PageNavigation = { | |
7003 | /** | |
7004 | * submit button | |
7005 | * @var jQuery | |
7006 | */ | |
7007 | _button: null, | |
7008 | ||
7009 | /** | |
7010 | * page No description | |
7011 | * @var jQuery | |
7012 | */ | |
7013 | _description: null, | |
7014 | ||
7015 | /** | |
7016 | * dialog overlay | |
7017 | * @var jQuery | |
7018 | */ | |
7019 | _dialog: null, | |
7020 | ||
7021 | /** | |
7022 | * active element id | |
7023 | * @var string | |
7024 | */ | |
7025 | _elementID: '', | |
7026 | ||
7027 | /** | |
7028 | * list of tracked navigation bars | |
7029 | * @var object | |
7030 | */ | |
7031 | _elements: { }, | |
7032 | ||
7033 | /** | |
7034 | * page No input | |
7035 | * @var jQuery | |
7036 | */ | |
7037 | _pageNo: null, | |
7038 | ||
7039 | /** | |
7040 | * Initializes the 'jump to page' overlay for given selector. | |
7041 | * | |
7042 | * @param string selector | |
3bdc6920 | 7043 | * @param object callback |
d83e246c | 7044 | */ |
3bdc6920 | 7045 | init: function(selector, callback) { |
d83e246c AE |
7046 | var $elements = $(selector); |
7047 | if (!$elements.length) { | |
7048 | return; | |
7049 | } | |
7050 | ||
3bdc6920 | 7051 | callback = callback || null; |
3bdc6920 AE |
7052 | if (callback !== null && !$.isFunction(callback)) { |
7053 | console.debug("[WCF.System.PageNavigation] Callback for selector '" + selector + "' is invalid, aborting."); | |
7054 | return; | |
7055 | } | |
7056 | ||
7057 | this._initElements($elements, callback); | |
d83e246c AE |
7058 | }, |
7059 | ||
7060 | /** | |
7061 | * Initializes the 'jump to page' overlay for given elements. | |
7062 | * | |
7063 | * @param jQuery elements | |
3bdc6920 | 7064 | * @param object callback |
d83e246c | 7065 | */ |
3bdc6920 | 7066 | _initElements: function(elements, callback) { |
d83e246c AE |
7067 | var self = this; |
7068 | elements.each(function(index, element) { | |
7069 | var $element = $(element); | |
7070 | var $elementID = $element.wcfIdentify(); | |
25c2c382 | 7071 | |
d83e246c AE |
7072 | if (self._elements[$elementID] === undefined) { |
7073 | self._elements[$elementID] = $element; | |
7074 | $element.find('li.jumpTo').data('elementID', $elementID).click($.proxy(self._click, self)); | |
7075 | } | |
3bdc6920 | 7076 | }).data('callback', callback); |
d83e246c AE |
7077 | }, |
7078 | ||
7079 | /** | |
7080 | * Shows the 'jump to page' overlay. | |
7081 | * | |
7082 | * @param object event | |
7083 | */ | |
7084 | _click: function(event) { | |
7085 | this._elementID = $(event.currentTarget).data('elementID'); | |
7086 | ||
7087 | if (this._dialog === null) { | |
7088 | this._dialog = $('<div id="pageNavigationOverlay" />').hide().appendTo(document.body); | |
7089 | ||
7090 | var $fieldset = $('<fieldset><legend>' + WCF.Language.get('wcf.global.page.jumpTo') + '</legend></fieldset>').appendTo(this._dialog); | |
7091 | $('<dl><dt><label for="jsPageNavigationPageNo">' + WCF.Language.get('wcf.global.page.jumpTo') + '</label></dt><dd></dd></dl>').appendTo($fieldset); | |
adf4d051 | 7092 | this._pageNo = $('<input type="number" id="jsPageNavigationPageNo" value="1" min="1" max="1" class="tiny" />').keyup($.proxy(this._keyUp, this)).appendTo($fieldset.find('dd')); |
d83e246c AE |
7093 | this._description = $('<small></small>').insertAfter(this._pageNo); |
7094 | var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog); | |
7095 | this._button = $('<button class="buttonPrimary">' + WCF.Language.get('wcf.global.button.submit') + '</button>').click($.proxy(this._submit, this)).appendTo($formSubmit); | |
7096 | } | |
7097 | ||
7098 | this._button.enable(); | |
7099 | this._description.html(WCF.Language.get('wcf.global.page.jumpTo.description').replace(/#pages#/, this._elements[this._elementID].data('pages'))); | |
adf4d051 | 7100 | this._pageNo.val(this._elements[this._elementID].data('pages')).attr('max', this._elements[this._elementID].data('pages')); |
d83e246c AE |
7101 | |
7102 | this._dialog.wcfDialog({ | |
7103 | 'title': WCF.Language.get('wcf.global.page.pageNavigation') | |
7104 | }); | |
7105 | }, | |
7106 | ||
7107 | /** | |
7108 | * Validates the page No input. | |
1771750d MS |
7109 | * |
7110 | * @param Event event | |
d83e246c | 7111 | */ |
1771750d MS |
7112 | _keyUp: function(event) { |
7113 | if (event.which == $.ui.keyCode.ENTER && !this._button.prop('disabled')) { | |
7114 | this._submit(); | |
7115 | return; | |
7116 | } | |
7117 | ||
d83e246c AE |
7118 | var $pageNo = parseInt(this._pageNo.val()) || 0; |
7119 | if ($pageNo < 1 || $pageNo > this._pageNo.attr('max')) { | |
7120 | this._button.disable(); | |
7121 | } | |
7122 | else { | |
7123 | this._button.enable(); | |
7124 | } | |
7125 | }, | |
7126 | ||
7127 | /** | |
7128 | * Redirects to given page No. | |
7129 | */ | |
7130 | _submit: function() { | |
3bdc6920 AE |
7131 | var $pageNavigation = this._elements[this._elementID]; |
7132 | if ($pageNavigation.data('callback') === null) { | |
7133 | var $redirectURL = $pageNavigation.data('link').replace(/pageNo=%d/, 'pageNo=' + this._pageNo.val()); | |
7134 | window.location = $redirectURL; | |
7135 | } | |
7136 | else { | |
7137 | $pageNavigation.data('callback')(this._pageNo.val()); | |
a19475d3 | 7138 | this._dialog.wcfDialog('close'); |
3bdc6920 | 7139 | } |
d83e246c AE |
7140 | } |
7141 | }; | |
7142 | ||
dd932bc6 AE |
7143 | /** |
7144 | * Sends periodical requests to protect the session from expiring. By default | |
7145 | * it will send a request 1 minute before it would expire. | |
7146 | * | |
7147 | * @param integer seconds | |
7148 | */ | |
7149 | WCF.System.KeepAlive = Class.extend({ | |
7150 | /** | |
7151 | * Initializes the WCF.System.KeepAlive class. | |
7152 | * | |
7153 | * @param integer seconds | |
7154 | */ | |
7155 | init: function(seconds) { | |
9b1326f4 | 7156 | new WCF.PeriodicalExecuter(function(pe) { |
dd932bc6 AE |
7157 | new WCF.Action.Proxy({ |
7158 | autoSend: true, | |
7159 | data: { | |
7160 | actionName: 'keepAlive', | |
7161 | className: 'wcf\\data\\session\\SessionAction' | |
90b713ac | 7162 | }, |
9b1326f4 | 7163 | failure: function() { pe.stop(); }, |
88432fe0 AE |
7164 | showLoadingOverlay: false, |
7165 | suppressErrors: true | |
dd932bc6 AE |
7166 | }); |
7167 | }, (seconds * 1000)); | |
7168 | } | |
7169 | }); | |
7170 | ||
e7f87db1 AE |
7171 | /** |
7172 | * Default implementation for inline editors. | |
be8e935b AE |
7173 | * |
7174 | * @param string elementSelector | |
e7f87db1 AE |
7175 | */ |
7176 | WCF.InlineEditor = Class.extend({ | |
7177 | /** | |
7178 | * list of registered callbacks | |
7179 | * @var array<object> | |
7180 | */ | |
7181 | _callbacks: [ ], | |
7182 | ||
7c1fa02a AE |
7183 | /** |
7184 | * list of dropdown selections | |
7185 | * @var object | |
7186 | */ | |
7187 | _dropdowns: { }, | |
7188 | ||
e7f87db1 AE |
7189 | /** |
7190 | * list of container elements | |
7191 | * @var object | |
7192 | */ | |
7193 | _elements: { }, | |
7194 | ||
7195 | /** | |
7c1fa02a AE |
7196 | * notification object |
7197 | * @var WCF.System.Notification | |
e7f87db1 | 7198 | */ |
7c1fa02a | 7199 | _notification: null, |
e7f87db1 | 7200 | |
e7f87db1 AE |
7201 | /** |
7202 | * list of known options | |
7203 | * @var array<object> | |
7204 | */ | |
7205 | _options: [ ], | |
7206 | ||
7207 | /** | |
7208 | * action proxy | |
7209 | * @var WCF.Action.Proxy | |
7210 | */ | |
7211 | _proxy: null, | |
7212 | ||
f3ca0ddf AE |
7213 | /** |
7214 | * list of data to update upon success | |
7215 | * @var array<object> | |
7216 | */ | |
7217 | _updateData: [ ], | |
7218 | ||
e7f87db1 AE |
7219 | /** |
7220 | * Initializes a new inline editor. | |
7221 | */ | |
f3ca0ddf AE |
7222 | init: function(elementSelector) { |
7223 | var $elements = $(elementSelector); | |
e7f87db1 AE |
7224 | if (!$elements.length) { |
7225 | return; | |
7226 | } | |
7227 | ||
150fb135 AE |
7228 | this._setOptions(); |
7229 | var $quickOption = ''; | |
7230 | for (var $i = 0, $length = this._options.length; $i < $length; $i++) { | |
7231 | if (this._options[$i].isQuickOption) { | |
7232 | $quickOption = this._options[$i].optionName; | |
7233 | break; | |
7234 | } | |
7235 | } | |
7236 | ||
e7f87db1 AE |
7237 | var self = this; |
7238 | $elements.each(function(index, element) { | |
7239 | var $element = $(element); | |
7240 | var $elementID = $element.wcfIdentify(); | |
7241 | ||
7242 | // find trigger element | |
f3ca0ddf | 7243 | var $trigger = self._getTriggerElement($element); |
e7f87db1 AE |
7244 | if ($trigger === null || $trigger.length !== 1) { |
7245 | return; | |
7246 | } | |
7247 | ||
f3ca0ddf | 7248 | $trigger.click($.proxy(self._show, self)).data('elementID', $elementID); |
150fb135 AE |
7249 | if ($quickOption) { |
7250 | // simulate click on target action | |
7251 | $trigger.disableSelection().data('optionName', $quickOption).dblclick($.proxy(self._click, self)); | |
7252 | } | |
e7f87db1 AE |
7253 | |
7254 | // store reference | |
7255 | self._elements[$elementID] = $element; | |
7256 | }); | |
7257 | ||
7258 | this._proxy = new WCF.Action.Proxy({ | |
f3ca0ddf | 7259 | success: $.proxy(this._success, this) |
e7f87db1 | 7260 | }); |
f3ca0ddf | 7261 | |
83428b7f | 7262 | WCF.CloseOverlayHandler.addCallback('WCF.InlineEditor', $.proxy(this._closeAll, this)); |
7c1fa02a AE |
7263 | |
7264 | this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success'); | |
83428b7f AE |
7265 | }, |
7266 | ||
7267 | /** | |
7268 | * Closes all inline editors. | |
7269 | */ | |
7270 | _closeAll: function() { | |
7271 | for (var $elementID in this._elements) { | |
7272 | this._hide($elementID); | |
7273 | } | |
e7f87db1 AE |
7274 | }, |
7275 | ||
f3ca0ddf AE |
7276 | /** |
7277 | * Sets options for this inline editor. | |
7278 | */ | |
7279 | _setOptions: function() { | |
7280 | this._options = [ ]; | |
be8e935b | 7281 | }, |
f3ca0ddf | 7282 | |
e7f87db1 AE |
7283 | /** |
7284 | * Register an option callback for validation and execution. | |
7285 | * | |
7286 | * @param object callback | |
7287 | */ | |
7288 | registerCallback: function(callback) { | |
7289 | if ($.isFunction(callback)) { | |
7290 | this._callbacks.push(callback); | |
7291 | } | |
7292 | }, | |
7293 | ||
7294 | /** | |
7295 | * Returns the triggering element. | |
7296 | * | |
7297 | * @param jQuery element | |
7298 | * @return jQuery | |
7299 | */ | |
7300 | _getTriggerElement: function(element) { | |
7301 | return null; | |
7302 | }, | |
7303 | ||
7304 | /** | |
7305 | * Shows a dropdown menu if options are available. | |
7306 | * | |
7307 | * @param object event | |
7308 | */ | |
7309 | _show: function(event) { | |
7310 | var $elementID = $(event.currentTarget).data('elementID'); | |
f24f0823 | 7311 | |
3ef8dee9 AE |
7312 | // build dropdown |
7313 | var $trigger = null; | |
e7f87db1 | 7314 | if (!this._dropdowns[$elementID]) { |
3ef8dee9 AE |
7315 | $trigger = this._getTriggerElement(this._elements[$elementID]).addClass('dropdownToggle').wrap('<span class="dropdown" />'); |
7316 | this._dropdowns[$elementID] = $('<ul class="dropdownMenu" />').insertAfter($trigger); | |
e7f87db1 | 7317 | } |
84abb173 | 7318 | this._dropdowns[$elementID].empty(); |
e7f87db1 AE |
7319 | |
7320 | // validate options | |
7321 | var $hasOptions = false; | |
b3524514 | 7322 | var $lastElementType = ''; |
e7f87db1 AE |
7323 | for (var $i = 0, $length = this._options.length; $i < $length; $i++) { |
7324 | var $option = this._options[$i]; | |
7325 | ||
7c1fa02a | 7326 | if ($option.optionName === 'divider') { |
b3524514 AE |
7327 | if ($lastElementType !== '' && $lastElementType !== 'divider') { |
7328 | $('<li class="dropdownDivider" />').appendTo(this._dropdowns[$elementID]); | |
7329 | $lastElementType = $option.optionName; | |
7330 | } | |
7c1fa02a AE |
7331 | } |
7332 | else if (this._validate($elementID, $option.optionName) || this._validateCallbacks($elementID, $option.optionName)) { | |
e7f87db1 | 7333 | var $listItem = $('<li><span>' + $option.label + '</span></li>').appendTo(this._dropdowns[$elementID]); |
0dcde1e2 | 7334 | $listItem.data('elementID', $elementID).data('optionName', $option.optionName).data('isQuickOption', ($option.isQuickOption ? true : false)).click($.proxy(this._click, this)); |
e7f87db1 AE |
7335 | |
7336 | $hasOptions = true; | |
b3524514 | 7337 | $lastElementType = $option.optionName; |
e7f87db1 AE |
7338 | } |
7339 | } | |
7340 | ||
7341 | if ($hasOptions) { | |
4c5cfd1a MS |
7342 | // if last child is divider, remove it |
7343 | var $lastChild = this._dropdowns[$elementID].children().last(); | |
7344 | if ($lastChild.hasClass('dropdownDivider')) { | |
7345 | $lastChild.remove(); | |
7346 | } | |
0dcde1e2 AE |
7347 | |
7348 | // check if only element is a quick option | |
7349 | var $quickOption = null; | |
7350 | var $count = 0; | |
7351 | this._dropdowns[$elementID].children().each(function(index, child) { | |
7352 | var $child = $(child); | |
7353 | if (!$child.hasClass('dropdownDivider')) { | |
7354 | if ($child.data('isQuickOption')) { | |
7355 | $quickOption = $child; | |
7356 | } | |
7357 | else { | |
7358 | $count++; | |
7359 | } | |
7360 | } | |
7361 | }); | |
60dbffb8 | 7362 | |
0dcde1e2 AE |
7363 | if (!$count) { |
7364 | $quickOption.trigger('click'); | |
7365 | ||
5c653172 | 7366 | if ($trigger !== null) { |
0dcde1e2 AE |
7367 | WCF.Dropdown.close($trigger.parents('.dropdown').wcfIdentify()); |
7368 | } | |
7369 | ||
7370 | return false; | |
7371 | } | |
e7f87db1 | 7372 | } |
83428b7f | 7373 | |
3ef8dee9 AE |
7374 | if ($trigger !== null) { |
7375 | WCF.Dropdown.initDropdown($trigger, true); | |
7376 | } | |
7377 | ||
83428b7f | 7378 | return false; |
e7f87db1 AE |
7379 | }, |
7380 | ||
7381 | /** | |
7382 | * Validates an option. | |
7383 | * | |
7384 | * @param string elementID | |
7385 | * @param string optionName | |
7386 | * @returns boolean | |
7387 | */ | |
7388 | _validate: function(elementID, optionName) { | |
7389 | return false; | |
7390 | }, | |
7391 | ||
7392 | /** | |
7393 | * Validates an option provided by callbacks. | |
7394 | * | |
7395 | * @param string elementID | |
7396 | * @param string optionName | |
7397 | * @return boolean | |
7398 | */ | |
7399 | _validateCallbacks: function(elementID, optionName) { | |
7400 | var $length = this._callbacks.length; | |
7401 | if ($length) { | |
7402 | for (var $i = 0; $i < $length; $i++) { | |
7403 | if (this._callbacks[$i].validate(this._elements[elementID], optionName)) { | |
7404 | return true; | |
7405 | } | |
7406 | } | |
7407 | } | |
7408 | ||
7409 | return false; | |
7410 | }, | |
7411 | ||
7412 | /** | |
7413 | * Handles AJAX responses. | |
7414 | * | |
7415 | * @param object data | |
7416 | * @param string textStatus | |
7417 | * @param jQuery jqXHR | |
7418 | */ | |
f3ca0ddf AE |
7419 | _success: function(data, textStatus, jqXHR) { |
7420 | var $length = this._updateData.length; | |
7421 | if (!$length) { | |
7422 | return; | |
7423 | } | |
7424 | ||
8eace9e0 | 7425 | this._updateState(data); |
f3ca0ddf AE |
7426 | |
7427 | this._updateData = [ ]; | |
7428 | }, | |
7429 | ||
7430 | /** | |
7431 | * Update element states based upon update data. | |
8eace9e0 AE |
7432 | * |
7433 | * @param object data | |
f3ca0ddf | 7434 | */ |
8eace9e0 | 7435 | _updateState: function(data) { }, |
e7f87db1 AE |
7436 | |
7437 | /** | |
7438 | * Handles clicks within dropdown. | |
7439 | * | |
7440 | * @param object event | |
7441 | */ | |
7442 | _click: function(event) { | |
7443 | var $listItem = $(event.currentTarget); | |
7444 | var $elementID = $listItem.data('elementID'); | |
7445 | var $optionName = $listItem.data('optionName'); | |
7446 | ||
7447 | if (!this._execute($elementID, $optionName)) { | |
7448 | this._executeCallback($elementID, $optionName); | |
7449 | } | |
7450 | ||
7451 | this._hide($elementID); | |
7452 | }, | |
7453 | ||
7454 | /** | |
7455 | * Executes actions associated with an option. | |
7456 | * | |
7457 | * @param string elementID | |
7458 | * @param string optionName | |
7459 | * @return boolean | |
7460 | */ | |
7461 | _execute: function(elementID, optionName) { | |
7462 | return false; | |
7463 | }, | |
7464 | ||
7465 | /** | |
7466 | * Executes actions associated with an option provided by callbacks. | |
7467 | * | |
7468 | * @param string elementID | |
7469 | * @param string optionName | |
7470 | * @return boolean | |
7471 | */ | |
7472 | _executeCallback: function(elementID, optionName) { | |
7473 | var $length = this._callbacks.length; | |
7474 | if ($length) { | |
7475 | for (var $i = 0; $i < $length; $i++) { | |
7476 | if (this._callbacks[$i].execute(this._elements[elementID], optionName)) { | |
7477 | return true; | |
7478 | } | |
7479 | } | |
7480 | } | |
7481 | ||
7482 | return false; | |
7483 | }, | |
7484 | ||
7485 | /** | |
7486 | * Hides a dropdown menu. | |
7487 | * | |
7488 | * @param string elementID | |
7489 | */ | |
7490 | _hide: function(elementID) { | |
83428b7f | 7491 | if (this._dropdowns[elementID]) { |
21b08838 | 7492 | this._dropdowns[elementID].empty().removeClass('dropdownOpen'); |
83428b7f | 7493 | } |
e7f87db1 AE |
7494 | } |
7495 | }); | |
7496 | ||
c1aaf7a7 MW |
7497 | /** |
7498 | * Default implementation for ajax file uploads | |
7499 | * | |
7500 | * @param jquery buttonSelector | |
7501 | * @param jquery fileListSelector | |
7502 | * @param string className | |
7503 | * @param jquery options | |
7504 | */ | |
7505 | WCF.Upload = Class.extend({ | |
7506 | /** | |
7507 | * name of the upload field | |
9f959ced | 7508 | * @var string |
c1aaf7a7 MW |
7509 | */ |
7510 | _name: '__files[]', | |
7511 | ||
7512 | /** | |
a2f01563 | 7513 | * button selector |
9f959ced | 7514 | * @var jQuery |
c1aaf7a7 MW |
7515 | */ |
7516 | _buttonSelector: null, | |
7517 | ||
7518 | /** | |
a2f01563 | 7519 | * file list selector |
9f959ced | 7520 | * @var jQuery |
c1aaf7a7 MW |
7521 | */ |
7522 | _fileListSelector: null, | |
7523 | ||
7524 | /** | |
7525 | * upload file | |
9f959ced | 7526 | * @var jQuery |
c1aaf7a7 MW |
7527 | */ |
7528 | _fileUpload: null, | |
7529 | ||
7530 | /** | |
7531 | * class name | |
9f959ced | 7532 | * @var string |
c1aaf7a7 MW |
7533 | */ |
7534 | _className: '', | |
7535 | ||
e919d1d3 AE |
7536 | /** |
7537 | * iframe for IE<10 fallback | |
7538 | * @var jQuery | |
7539 | */ | |
7540 | _iframe: null, | |
7541 | ||
d9e09941 AE |
7542 | /** |
7543 | * internal file id | |
7544 | * @var integer | |
7545 | */ | |
7546 | _internalFileID: 0, | |
7547 | ||
c1aaf7a7 MW |
7548 | /** |
7549 | * additional options | |
9f959ced | 7550 | * @var jQuery |
c1aaf7a7 MW |
7551 | */ |
7552 | _options: {}, | |
7553 | ||
7554 | /** | |
7555 | * upload matrix | |
9f959ced | 7556 | * @var array |
c1aaf7a7 MW |
7557 | */ |
7558 | _uploadMatrix: [], | |
7559 | ||
7560 | /** | |
9f959ced MS |
7561 | * true, if the active user's browser supports ajax file uploads |
7562 | * @var boolean | |
c1aaf7a7 MW |
7563 | */ |
7564 | _supportsAJAXUpload: true, | |
7565 | ||
7566 | /** | |
7567 | * fallback overlay for stupid browsers | |
9f959ced | 7568 | * @var jquery |
c1aaf7a7 MW |
7569 | */ |
7570 | _overlay: null, | |
7571 | ||
7572 | /** | |
7573 | * Initializes a new upload handler. | |
e919d1d3 | 7574 | * |
a2f01563 AE |
7575 | * @param string buttonSelector |
7576 | * @param string fileListSelector | |
e919d1d3 AE |
7577 | * @param string className |
7578 | * @param object options | |
c1aaf7a7 MW |
7579 | */ |
7580 | init: function(buttonSelector, fileListSelector, className, options) { | |
7581 | this._buttonSelector = buttonSelector; | |
7582 | this._fileListSelector = fileListSelector; | |
7583 | this._className = className; | |
d9e09941 | 7584 | this._internalFileID = 0; |
c1aaf7a7 MW |
7585 | this._options = $.extend(true, { |
7586 | action: 'upload', | |
7587 | multiple: false, | |
7588 | url: 'index.php/AJAXUpload/?t=' + SECURITY_TOKEN + SID_ARG_2ND | |
e919d1d3 | 7589 | }, options || { }); |
c1aaf7a7 MW |
7590 | |
7591 | // check for ajax upload support | |
7592 | var $xhr = new XMLHttpRequest(); | |
7593 | this._supportsAJAXUpload = ($xhr && ('upload' in $xhr) && ('onprogress' in $xhr.upload)); | |
7594 | ||
7595 | // create upload button | |
7596 | this._createButton(); | |
7597 | }, | |
7598 | ||
7599 | /** | |
7600 | * Creates the upload button. | |
7601 | */ | |
7602 | _createButton: function() { | |
7603 | if (this._supportsAJAXUpload) { | |
0d2b7a3b | 7604 | this._fileUpload = $('<input type="file" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/>'); |
c1aaf7a7 | 7605 | this._fileUpload.change($.proxy(this._upload, this)); |
0d2b7a3b | 7606 | var $button = $('<p class="button uploadButton"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>'); |
5427002e | 7607 | $button.prepend(this._fileUpload); |
c1aaf7a7 MW |
7608 | } |
7609 | else { | |
0d2b7a3b | 7610 | var $button = $('<p class="button uploadFallbackButton"><span>' + WCF.Language.get('wcf.global.button.upload') + '</span></p>'); |
c1aaf7a7 MW |
7611 | $button.click($.proxy(this._showOverlay, this)); |
7612 | } | |
7613 | ||
7614 | this._insertButton($button); | |
7615 | }, | |
7616 | ||
7617 | /** | |
7618 | * Inserts the upload button. | |
e919d1d3 AE |
7619 | * |
7620 | * @param jQuery button | |
c1aaf7a7 MW |
7621 | */ |
7622 | _insertButton: function(button) { | |
7623 | this._buttonSelector.append(button); | |
7624 | }, | |
7625 | ||
0d2b7a3b MS |
7626 | /** |
7627 | * Removes the upload button. | |
7628 | */ | |
7629 | _removeButton: function() { | |
7630 | var $selector = '.uploadButton'; | |
7631 | if (!this._supportsAJAXUpload) { | |
7632 | $selector = '.uploadFallbackButton'; | |
7633 | } | |
7634 | ||
7635 | this._buttonSelector.find($selector).remove(); | |
7636 | }, | |
7637 | ||
c1aaf7a7 MW |
7638 | /** |
7639 | * Callback for file uploads. | |
7640 | */ | |
7641 | _upload: function() { | |
7642 | var $files = this._fileUpload.prop('files'); | |
e919d1d3 | 7643 | if ($files.length) { |
c1aaf7a7 | 7644 | var $fd = new FormData(); |
e919d1d3 | 7645 | var $uploadID = this._createUploadMatrix($files); |
c1aaf7a7 | 7646 | |
404c9f0d AE |
7647 | // no more files left, abort |
7648 | if (!this._uploadMatrix[$uploadID].length) { | |
7649 | return; | |
7650 | } | |
7651 | ||
e919d1d3 | 7652 | for (var $i = 0, $length = $files.length; $i < $length; $i++) { |
d9e09941 AE |
7653 | if (this._uploadMatrix[$uploadID][$i]) { |
7654 | var $internalFileID = this._uploadMatrix[$uploadID][$i].data('internalFileID'); | |
7655 | $fd.append('__files[' + $internalFileID + ']', $files[$i]); | |
404c9f0d | 7656 | } |
c1aaf7a7 | 7657 | } |
e919d1d3 | 7658 | |
c1aaf7a7 MW |
7659 | $fd.append('actionName', this._options.action); |
7660 | $fd.append('className', this._className); | |
32f6cd95 MW |
7661 | var $additionalParameters = this._getParameters(); |
7662 | for (var $name in $additionalParameters) { | |
d9e09941 | 7663 | $fd.append('parameters[' + $name + ']', $additionalParameters[$name]); |
32f6cd95 | 7664 | } |
c1aaf7a7 | 7665 | |
e919d1d3 | 7666 | var self = this; |
c1aaf7a7 MW |
7667 | $.ajax({ |
7668 | type: 'POST', | |
7669 | url: this._options.url, | |
7670 | enctype: 'multipart/form-data', | |
7671 | data: $fd, | |
7672 | contentType: false, | |
7673 | processData: false, | |
32f6cd95 MW |
7674 | success: function(data, textStatus, jqXHR) { |
7675 | self._success($uploadID, data); | |
7676 | }, | |
c1aaf7a7 MW |
7677 | error: $.proxy(this._error, this), |
7678 | xhr: function() { | |
7679 | var $xhr = $.ajaxSettings.xhr(); | |
7680 | if ($xhr) { | |
7681 | $xhr.upload.addEventListener('progress', function(event) { | |
32f6cd95 | 7682 | self._progress($uploadID, event); |
c1aaf7a7 MW |
7683 | }, false); |
7684 | } | |
7685 | return $xhr; | |
7686 | } | |
7687 | }); | |
7688 | } | |
7689 | }, | |
7690 | ||
7691 | /** | |
e919d1d3 AE |
7692 | * Creates upload matrix for provided files. |
7693 | * | |
7694 | * @param array<object> files | |
7695 | * @return integer | |
c1aaf7a7 | 7696 | */ |
e919d1d3 AE |
7697 | _createUploadMatrix: function(files) { |
7698 | if (files.length) { | |
7699 | var $uploadID = this._uploadMatrix.length; | |
7700 | this._uploadMatrix[$uploadID] = [ ]; | |
7701 | ||
7702 | for (var $i = 0, $length = files.length; $i < $length; $i++) { | |
7703 | var $file = files[$i]; | |
7704 | var $li = this._initFile($file); | |
7705 | ||
404c9f0d | 7706 | if (!$li.hasClass('uploadFailed')) { |
d9e09941 AE |
7707 | $li.data('filename', $file.name).data('internalFileID', this._internalFileID++); |
7708 | this._uploadMatrix[$uploadID][$i] = $li; | |
404c9f0d | 7709 | } |
e919d1d3 AE |
7710 | } |
7711 | ||
7712 | return $uploadID; | |
7713 | } | |
7714 | ||
7715 | return null; | |
c1aaf7a7 MW |
7716 | }, |
7717 | ||
7718 | /** | |
e919d1d3 AE |
7719 | * Callback for success event. |
7720 | * | |
7721 | * @param integer uploadID | |
7722 | * @param object data | |
c1aaf7a7 | 7723 | */ |
e919d1d3 | 7724 | _success: function(uploadID, data) { }, |
c1aaf7a7 MW |
7725 | |
7726 | /** | |
e919d1d3 AE |
7727 | * Callback for error event. |
7728 | * | |
7729 | * @param jQuery jqXHR | |
7730 | * @param string textStatus | |
7731 | * @param string errorThrown | |
7732 | */ | |
7733 | _error: function(jqXHR, textStatus, errorThrown) { }, | |
7734 | ||
7735 | /** | |
7736 | * Callback for progress event. | |
7737 | * | |
7738 | * @param integer uploadID | |
7739 | * @param object event | |
c1aaf7a7 | 7740 | */ |
32f6cd95 | 7741 | _progress: function(uploadID, event) { |
c1aaf7a7 MW |
7742 | var $percentComplete = Math.round(event.loaded * 100 / event.total); |
7743 | ||
d9e09941 | 7744 | for (var $i in this._uploadMatrix[uploadID]) { |
c1aaf7a7 MW |
7745 | this._uploadMatrix[uploadID][$i].find('progress').attr('value', $percentComplete); |
7746 | } | |
7747 | }, | |
7748 | ||
32f6cd95 MW |
7749 | /** |
7750 | * Returns additional parameters. | |
e919d1d3 AE |
7751 | * |
7752 | * @return object | |
32f6cd95 MW |
7753 | */ |
7754 | _getParameters: function() { | |
7755 | return {}; | |
7756 | }, | |
c1aaf7a7 | 7757 | |
e919d1d3 AE |
7758 | /** |
7759 | * Initializes list item for uploaded file. | |
7760 | * | |
7761 | * @return jQuery | |
7762 | */ | |
c1aaf7a7 | 7763 | _initFile: function(file) { |
e919d1d3 | 7764 | return $('<li>' + file.name + ' (' + file.size + ')<progress max="100" /></li>').appendTo(this._fileListSelector); |
c1aaf7a7 MW |
7765 | }, |
7766 | ||
7767 | /** | |
7768 | * Shows the fallback overlay (work in progress) | |
7769 | */ | |
7770 | _showOverlay: function() { | |
c1aaf7a7 | 7771 | // create iframe |
e919d1d3 AE |
7772 | if (this._iframe === null) { |
7773 | this._iframe = $('<iframe name="__fileUploadIFrame" />').hide().appendTo(document.body); | |
7774 | } | |
c1aaf7a7 | 7775 | |
e919d1d3 AE |
7776 | // create overlay |
7777 | if (!this._overlay) { | |
7778 | this._overlay = $('<div><form enctype="multipart/form-data" method="post" action="' + this._options.url + '" target="__fileUploadIFrame" /></div>').hide().appendTo(document.body); | |
7779 | ||
7780 | var $form = this._overlay.find('form'); | |
5427002e | 7781 | $('<dl class="wide"><dd><input type="file" id="__fileUpload" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/></dd></dl>').appendTo($form); |
e919d1d3 AE |
7782 | $('<div class="formSubmit"><input type="submit" value="Upload" accesskey="s" /></div></form>').appendTo($form); |
7783 | ||
7784 | $('<input type="hidden" name="isFallback" value="1" />').appendTo($form); | |
7785 | $('<input type="hidden" name="actionName" value="' + this._options.action + '" />').appendTo($form); | |
7786 | $('<input type="hidden" name="className" value="' + this._className + '" />').appendTo($form); | |
7787 | var $additionalParameters = this._getParameters(); | |
7788 | for (var $name in $additionalParameters) { | |
7789 | $('<input type="hidden" name="' + $name + '" value="' + $additionalParameters[$name] + '" />').appendTo($form); | |
7790 | } | |
7791 | ||
7792 | $form.submit($.proxy(function() { | |
7793 | var $file = { | |
7794 | name: this._getFilename(), | |
5427002e | 7795 | size: '' |
e919d1d3 AE |
7796 | }; |
7797 | ||
7798 | var $uploadID = this._createUploadMatrix([ $file ]); | |
7799 | var self = this; | |
7800 | this._iframe.data('loading', true).off('load').load(function() { self._evaluateResponse($uploadID); }); | |
7801 | this._overlay.wcfDialog('close'); | |
7802 | }, this)); | |
7803 | } | |
c1aaf7a7 MW |
7804 | |
7805 | this._overlay.wcfDialog({ | |
5427002e | 7806 | title: WCF.Language.get('wcf.global.button.upload') |
c1aaf7a7 | 7807 | }); |
e919d1d3 AE |
7808 | }, |
7809 | ||
7810 | /** | |
7811 | * Evaluates iframe response. | |
7812 | * | |
7813 | * @param integer uploadID | |
7814 | */ | |
7815 | _evaluateResponse: function(uploadID) { | |
7816 | var $returnValues = $.parseJSON(this._iframe.contents().find('pre').html()); | |
7817 | this._success(uploadID, $returnValues); | |
7818 | }, | |
7819 | ||
7820 | /** | |
7821 | * Returns name of selected file. | |
7822 | * | |
7823 | * @return string | |
7824 | */ | |
7825 | _getFilename: function() { | |
7826 | return $('#__fileUpload').val().split('\\').pop(); | |
c1aaf7a7 MW |
7827 | } |
7828 | }); | |
7829 | ||
dedc6a49 MS |
7830 | /** |
7831 | * Default implementation for parallel AJAX file uploads. | |
7832 | */ | |
7833 | WCF.Upload.Parallel = WCF.Upload.extend({ | |
7834 | /** | |
7835 | * @see WCF.Upload.init() | |
7836 | */ | |
7837 | init: function(buttonSelector, fileListSelector, className, options) { | |
7838 | // force multiple uploads | |
5eaa16b7 MS |
7839 | options = $.extend(true, options || { }, { |
7840 | multiple: true | |
7841 | }); | |
dedc6a49 MS |
7842 | |
7843 | this._super(buttonSelector, fileListSelector, className, options); | |
7844 | }, | |
7845 | ||
7846 | /** | |
7847 | * @see WCF.Upload._upload() | |
7848 | */ | |
7849 | _upload: function() { | |
7850 | var $files = this._fileUpload.prop('files'); | |
7851 | for (var $i = 0, $length = $files.length; $i < $length; $i++) { | |
7852 | var $file = $files[$i]; | |
7853 | var $formData = new FormData(); | |
7854 | var $internalFileID = this._createUploadMatrix($file); | |
7855 | ||
7856 | if (!this._uploadMatrix[$internalFileID].length) { | |
7857 | continue; | |
7858 | } | |
7859 | ||
7860 | $formData.append('__files[' + $internalFileID + ']', $file); | |
7861 | $formData.append('actionName', this._options.action); | |
7862 | $formData.append('className', this._className); | |
7863 | var $additionalParameters = this._getParameters(); | |
7864 | for (var $name in $additionalParameters) { | |
7865 | $formData.append('parameters[' + $name + ']', $additionalParameters[$name]); | |
7866 | } | |
7867 | ||
7868 | this._sendRequest($internalFileID, $formData); | |
7869 | } | |
7870 | }, | |
7871 | ||
7872 | /** | |
7873 | * Sends an AJAX request to upload a file. | |
7874 | * | |
7875 | * @param integer internalFileID | |
7876 | * @param FormData formData | |
7877 | */ | |
7878 | _sendRequest: function(internalFileID, formData) { | |
7879 | var self = this; | |
7880 | $.ajax({ | |
7881 | type: 'POST', | |
7882 | url: this._options.url, | |
7883 | enctype: 'multipart/form-data', | |
7884 | data: formData, | |
7885 | contentType: false, | |
7886 | processData: false, | |
7887 | success: function(data, textStatus, jqXHR) { | |
7888 | self._success(internalFileID, data); | |
7889 | }, | |
7890 | error: $.proxy(this._error, this), | |
7891 | xhr: function() { | |
7892 | var $xhr = $.ajaxSettings.xhr(); | |
7893 | if ($xhr) { | |
7894 | $xhr.upload.addEventListener('progress', function(event) { | |
7895 | self._progress(internalFileID, event); | |
7896 | }, false); | |
7897 | } | |
7898 | return $xhr; | |
7899 | } | |
7900 | }); | |
7901 | }, | |
7902 | ||
7903 | /** | |
7904 | * Creates upload matrix for provided file and returns its internal file id. | |
7905 | * | |
7906 | * @param object file | |
7907 | * @return integer | |
7908 | */ | |
7909 | _createUploadMatrix: function(file) { | |
7910 | var $li = this._initFile(file); | |
7911 | if (!$li.hasClass('uploadFailed')) { | |
7912 | $li.data('filename', file.name).data('internalFileID', this._internalFileID); | |
7913 | this._uploadMatrix[this._internalFileID++] = $li; | |
7914 | ||
7915 | return this._internalFileID - 1; | |
7916 | } | |
7917 | ||
7918 | return null; | |
7919 | }, | |
7920 | ||
7921 | /** | |
7922 | * Callback for success event. | |
7923 | * | |
7924 | * @param integer internalFileID | |
7925 | * @param object data | |
7926 | */ | |
7927 | _success: function(internalFileID, data) { }, | |
7928 | ||
7929 | /** | |
7930 | * Callback for progress event. | |
7931 | * | |
7932 | * @param integer internalFileID | |
7933 | * @param object event | |
7934 | */ | |
7935 | _progress: function(internalFileID, event) { | |
7936 | var $percentComplete = Math.round(event.loaded * 100 / event.total); | |
7937 | ||
7938 | this._uploadMatrix[internalFileID].find('progress').attr('value', $percentComplete); | |
7939 | }, | |
7940 | ||
7941 | /** | |
7942 | * @see WCF.Upload._showOverlay() | |
7943 | */ | |
7944 | _showOverlay: function() { | |
7945 | // create iframe | |
7946 | if (this._iframe === null) { | |
7947 | this._iframe = $('<iframe name="__fileUploadIFrame" />').hide().appendTo(document.body); | |
7948 | } | |
7949 | ||
7950 | // create overlay | |
7951 | if (!this._overlay) { | |
7952 | this._overlay = $('<div><form enctype="multipart/form-data" method="post" action="' + this._options.url + '" target="__fileUploadIFrame" /></div>').hide().appendTo(document.body); | |
7953 | ||
7954 | var $form = this._overlay.find('form'); | |
7955 | $('<dl class="wide"><dd><input type="file" id="__fileUpload" name="' + this._name + '" ' + (this._options.multiple ? 'multiple="true" ' : '') + '/></dd></dl>').appendTo($form); | |
7956 | $('<div class="formSubmit"><input type="submit" value="Upload" accesskey="s" /></div></form>').appendTo($form); | |
7957 | ||
7958 | $('<input type="hidden" name="isFallback" value="1" />').appendTo($form); | |
7959 | $('<input type="hidden" name="actionName" value="' + this._options.action + '" />').appendTo($form); | |
7960 | $('<input type="hidden" name="className" value="' + this._className + '" />').appendTo($form); | |
7961 | var $additionalParameters = this._getParameters(); | |
7962 | for (var $name in $additionalParameters) { | |
7963 | $('<input type="hidden" name="' + $name + '" value="' + $additionalParameters[$name] + '" />').appendTo($form); | |
7964 | } | |
7965 | ||
7966 | $form.submit($.proxy(function() { | |
7967 | var $file = { | |
7968 | name: this._getFilename(), | |
7969 | size: '' | |
7970 | }; | |
7971 | ||
7972 | var $internalFileID = this._createUploadMatrix($file); | |
7973 | var self = this; | |
7974 | this._iframe.data('loading', true).off('load').load(function() { self._evaluateResponse($internalFileID); }); | |
7975 | this._overlay.wcfDialog('close'); | |
7976 | }, this)); | |
7977 | } | |
7978 | ||
7979 | this._overlay.wcfDialog({ | |
7980 | title: WCF.Language.get('wcf.global.button.upload') | |
7981 | }); | |
7982 | }, | |
7983 | ||
7984 | /** | |
7985 | * Evaluates iframe response. | |
7986 | * | |
7987 | * @param integer internalFileID | |
7988 | */ | |
7989 | _evaluateResponse: function(internalFileID) { | |
7990 | var $returnValues = $.parseJSON(this._iframe.contents().find('pre').html()); | |
7991 | this._success(internalFileID, $returnValues); | |
7992 | } | |
7993 | }); | |
7994 | ||
ee1a6ccb AE |
7995 | /** |
7996 | * Namespace for sortables. | |
7997 | */ | |
dedc6a49 | 7998 | WCF.Sortable = { }; |
ee1a6ccb AE |
7999 | |
8000 | /** | |
8001 | * Sortable implementation for lists. | |
8002 | * | |
8003 | * @param string containerID | |
8004 | * @param string className | |
3926f3e3 AE |
8005 | * @param integer offset |
8006 | * @param object options | |
ee1a6ccb | 8007 | */ |
3926f3e3 | 8008 | WCF.Sortable.List = Class.extend({ |
2a16194a AE |
8009 | /** |
8010 | * additional parameters for AJAX request | |
8011 | * @var object | |
8012 | */ | |
8013 | _additionalParameters: { }, | |
8014 | ||
ee1a6ccb AE |
8015 | /** |
8016 | * action class name | |
8017 | * @var string | |
8018 | */ | |
8019 | _className: '', | |
8020 | ||
8021 | /** | |
8022 | * container id | |
8023 | * @var string | |
8024 | */ | |
8025 | _containerID: '', | |
8026 | ||
8027 | /** | |
8028 | * container object | |
8029 | * @var jQuery | |
8030 | */ | |
8031 | _container: null, | |
8032 | ||
8033 | /** | |
8034 | * notification object | |
8035 | * @var WCF.System.Notification | |
8036 | */ | |
8037 | _notification: null, | |
8038 | ||
b007ce67 AE |
8039 | /** |
8040 | * show order offset | |
8041 | * @var integer | |
8042 | */ | |
8043 | _offset: 0, | |
8044 | ||
076e8a3f AE |
8045 | /** |
8046 | * list of options | |
8047 | * @var object | |
8048 | */ | |
8049 | _options: { }, | |
8050 | ||
ee1a6ccb AE |
8051 | /** |
8052 | * proxy object | |
8053 | * @var WCF.Action.Proxy | |
8054 | */ | |
8055 | _proxy: null, | |
8056 | ||
8057 | /** | |
8058 | * object structure | |
8059 | * @var object | |
8060 | */ | |
8061 | _structure: { }, | |
8062 | ||
8063 | /** | |
8064 | * Creates a new sortable list. | |
8065 | * | |
8066 | * @param string containerID | |
8067 | * @param string className | |
b007ce67 | 8068 | * @param integer offset |
076e8a3f | 8069 | * @param object options |
afa0b934 | 8070 | * @param boolean isSimpleSorting |
2a16194a | 8071 | * @param object additionalParameters |
ee1a6ccb | 8072 | */ |
2a16194a AE |
8073 | init: function(containerID, className, offset, options, isSimpleSorting, additionalParameters) { |
8074 | this._additionalParameters = additionalParameters || { }; | |
ee1a6ccb AE |
8075 | this._containerID = $.wcfEscapeID(containerID); |
8076 | this._container = $('#' + this._containerID); | |
8077 | this._className = className; | |
b007ce67 | 8078 | this._offset = (offset) ? offset : 0; |
ee1a6ccb AE |
8079 | this._proxy = new WCF.Action.Proxy({ |
8080 | success: $.proxy(this._success, this) | |
8081 | }); | |
8082 | this._structure = { }; | |
8083 | ||
8084 | // init sortable | |
076e8a3f | 8085 | this._options = $.extend(true, { |
9dcf11e2 | 8086 | axis: 'y', |
afa0b934 | 8087 | connectWith: '#' + this._containerID + ' .sortableList', |
3d6b3f29 | 8088 | disableNesting: 'sortableNoNesting', |
2d292d09 | 8089 | doNotClear: true, |
3d6b3f29 | 8090 | errorClass: 'sortableInvalidTarget', |
9dcf11e2 AE |
8091 | forcePlaceholderSize: true, |
8092 | helper: 'clone', | |
3d6b3f29 | 8093 | items: 'li:not(.sortableNoSorting)', |
9dcf11e2 | 8094 | opacity: .6, |
3d6b3f29 | 8095 | placeholder: 'sortablePlaceholder', |
9dcf11e2 AE |
8096 | tolerance: 'pointer', |
8097 | toleranceElement: '> span' | |
076e8a3f | 8098 | }, options || { }); |
afa0b934 AE |
8099 | |
8100 | if (isSimpleSorting) { | |
8101 | $('#' + this._containerID + ' .sortableList').sortable(this._options); | |
8102 | } | |
8103 | else { | |
2d292d09 | 8104 | $('#' + this._containerID + ' > .sortableList').nestedSortable(this._options); |
afa0b934 | 8105 | } |
d1a6b448 | 8106 | |
6d8f21ac | 8107 | if (this._className) { |
a6e646a3 AE |
8108 | var $formSubmit = this._container.find('.formSubmit'); |
8109 | if (!$formSubmit.length) { | |
8110 | $formSubmit = this._container.next('.formSubmit'); | |
8111 | if (!$formSubmit.length) { | |
8112 | console.debug("[WCF.Sortable.Simple] Unable to find form submit for saving, aborting."); | |
8113 | return; | |
8114 | } | |
8115 | } | |
8116 | ||
8117 | $formSubmit.children('button[data-type="submit"]').click($.proxy(this._submit, this)); | |
6d8f21ac | 8118 | } |
ee1a6ccb AE |
8119 | }, |
8120 | ||
8121 | /** | |
8122 | * Saves object structure. | |
8123 | */ | |
d1a6b448 | 8124 | _submit: function() { |
2a16194a AE |
8125 | // reset structure |
8126 | this._structure = { }; | |
8127 | ||
ee1a6ccb | 8128 | // build structure |
3d6b3f29 | 8129 | this._container.find('.sortableList').each($.proxy(function(index, list) { |
ee1a6ccb AE |
8130 | var $list = $(list); |
8131 | var $parentID = $list.data('objectID'); | |
8132 | ||
83a29d77 AE |
8133 | if ($parentID !== undefined) { |
8134 | $list.children(this._options.items).each($.proxy(function(index, listItem) { | |
8135 | var $objectID = $(listItem).data('objectID'); | |
8136 | ||
8137 | if (!this._structure[$parentID]) { | |
8138 | this._structure[$parentID] = [ ]; | |
8139 | } | |
8140 | ||
8141 | this._structure[$parentID].push($objectID); | |
8142 | }, this)); | |
8143 | } | |
ee1a6ccb AE |
8144 | }, this)); |
8145 | ||
8146 | // send request | |
2a16194a | 8147 | var $parameters = $.extend(true, { |
2a437fb3 MS |
8148 | data: { |
8149 | offset: this._offset, | |
8150 | structure: this._structure | |
8151 | } | |
2a16194a AE |
8152 | }, this._additionalParameters); |
8153 | ||
8154 | this._proxy.setOption('data', { | |
8155 | actionName: 'updatePosition', | |
8156 | className: this._className, | |
cea5b918 | 8157 | interfaceName: 'wcf\\data\\ISortableAction', |
2a16194a | 8158 | parameters: $parameters |
ee1a6ccb AE |
8159 | }); |
8160 | this._proxy.sendRequest(); | |
8161 | }, | |
8162 | ||
8163 | /** | |
8164 | * Shows notification upon success. | |
8165 | * | |
8166 | * @param object data | |
8167 | * @param string textStatus | |
8168 | * @param jQuery jqXHR | |
8169 | */ | |
8170 | _success: function(data, textStatus, jqXHR) { | |
8171 | if (this._notification === null) { | |
9b566f66 | 8172 | this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.edit')); |
ee1a6ccb AE |
8173 | } |
8174 | ||
8175 | this._notification.show(); | |
8176 | } | |
3926f3e3 | 8177 | }); |
ee1a6ccb | 8178 | |
453aced6 AE |
8179 | WCF.Popover = Class.extend({ |
8180 | /** | |
8181 | * currently active element id | |
8182 | * @var string | |
8183 | */ | |
8184 | _activeElementID: '', | |
8185 | ||
2f326e85 AE |
8186 | /** |
8187 | * cancels popover | |
8188 | * @var boolean | |
8189 | */ | |
8190 | _cancelPopover: false, | |
8191 | ||
453aced6 AE |
8192 | /** |
8193 | * element data | |
8194 | * @var object | |
8195 | */ | |
8196 | _data: { }, | |
8197 | ||
8198 | /** | |
8199 | * default dimensions, should reflect the estimated size | |
8200 | * @var object | |
8201 | */ | |
8202 | _defaultDimensions: { | |
523908ab | 8203 | height: 150, |
b85dd964 | 8204 | width: 450 |
453aced6 AE |
8205 | }, |
8206 | ||
8207 | /** | |
8208 | * default orientation, may be a combintion of left/right and bottom/top | |
8209 | * @var object | |
8210 | */ | |
8211 | _defaultOrientation: { | |
8212 | x: 'right', | |
8213 | y: 'top' | |
8214 | }, | |
8215 | ||
8216 | /** | |
8217 | * delay to show or hide popover, values in miliseconds | |
8218 | * @var object | |
8219 | */ | |
8220 | _delay: { | |
cb22c910 | 8221 | show: 800, |
453aced6 AE |
8222 | hide: 500 |
8223 | }, | |
8224 | ||
8225 | /** | |
8226 | * true, if an element is being hovered | |
8227 | * @var boolean | |
8228 | */ | |
8229 | _hoverElement: false, | |
8230 | ||
8231 | /** | |
8232 | * element id of element being hovered | |
8233 | * @var string | |
8234 | */ | |
8235 | _hoverElementID: '', | |
8236 | ||
8237 | /** | |
8238 | * true, if popover is being hovered | |
8239 | * @var boolean | |
8240 | */ | |
8241 | _hoverPopover: false, | |
8242 | ||
8243 | /** | |
8244 | * minimum margin (all directions) for popover | |
8245 | * @var integer | |
8246 | */ | |
8247 | _margin: 20, | |
8248 | ||
523908ab AE |
8249 | /** |
8250 | * periodical executer once element or popover is no longer being hovered | |
8251 | * @var WCF.PeriodicalExecuter | |
8252 | */ | |
8253 | _peOut: null, | |
8254 | ||
453aced6 AE |
8255 | /** |
8256 | * periodical executer once an element is being hovered | |
8257 | * @var WCF.PeriodicalExecuter | |
8258 | */ | |
8259 | _peOverElement: null, | |
8260 | ||
8261 | /** | |
8262 | * popover object | |
8263 | * @var jQuery | |
8264 | */ | |
8265 | _popover: null, | |
8266 | ||
5b755307 AE |
8267 | /** |
8268 | * popover content | |
8269 | * @var jQuery | |
8270 | */ | |
8271 | _popoverContent: null, | |
8272 | ||
453aced6 AE |
8273 | /** |
8274 | * popover horizontal offset | |
8275 | * @var integer | |
8276 | */ | |
8277 | _popoverOffset: 10, | |
8278 | ||
8279 | /** | |
8280 | * element selector | |
8281 | * @var string | |
8282 | */ | |
8283 | _selector: '', | |
8284 | ||
8285 | /** | |
8286 | * Initializes a new WCF.Popover object. | |
8287 | * | |
8288 | * @param string selector | |
8289 | */ | |
8290 | init: function(selector) { | |
3f42eecb MW |
8291 | if ($.browser.mobile) return; |
8292 | ||
453aced6 AE |
8293 | // assign default values |
8294 | this._activeElementID = ''; | |
2f326e85 | 8295 | this._cancelPopover = false; |
453aced6 AE |
8296 | this._data = { }; |
8297 | this._defaultDimensions = { | |
523908ab AE |
8298 | height: 150, |
8299 | width: 450 | |
453aced6 AE |
8300 | }; |
8301 | this._defaultOrientation = { | |
8302 | x: 'right', | |
8303 | y: 'top' | |
8304 | }; | |
8305 | this._delay = { | |
cb22c910 | 8306 | show: 800, |
453aced6 AE |
8307 | hide: 500 |
8308 | }; | |
8309 | this._hoverElement = false; | |
8310 | this._hoverElementID = ''; | |
8311 | this._hoverPopover = false; | |
8312 | this._margin = 20; | |
523908ab AE |
8313 | this._peOut = null; |
8314 | this._peOverElement = null; | |
453aced6 AE |
8315 | this._popoverOffset = 10; |
8316 | this._selector = selector; | |
8317 | ||
556973c1 | 8318 | this._popover = $('<div class="popover"><span class="icon icon48 icon-spinner"></span><div class="popoverContent"></div></div>').hide().appendTo(document.body); |
5b755307 | 8319 | this._popoverContent = this._popover.children('.popoverContent:eq(0)'); |
523908ab | 8320 | this._popover.hover($.proxy(this._overPopover, this), $.proxy(this._out, this)); |
453aced6 AE |
8321 | |
8322 | this._initContainers(); | |
3c1f2137 | 8323 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Popover.'+selector, $.proxy(this._initContainers, this)); |
453aced6 AE |
8324 | }, |
8325 | ||
8326 | /** | |
8327 | * Initializes all element triggers. | |
8328 | */ | |
8329 | _initContainers: function() { | |
3f42eecb MW |
8330 | if ($.browser.mobile) return; |
8331 | ||
453aced6 AE |
8332 | var $elements = $(this._selector); |
8333 | if (!$elements.length) { | |
8334 | return; | |
8335 | } | |
8336 | ||
8337 | $elements.each($.proxy(function(index, element) { | |
8338 | var $element = $(element); | |
8339 | var $elementID = $element.wcfIdentify(); | |
8340 | ||
8341 | if (!this._data[$elementID]) { | |
8342 | this._data[$elementID] = { | |
8343 | 'content': null, | |
8344 | 'isLoading': false | |
8345 | }; | |
8346 | ||
8347 | $element.hover($.proxy(this._overElement, this), $.proxy(this._out, this)); | |
2f326e85 | 8348 | |
404c9abe | 8349 | if ($element.is('a') && $element.attr('href')) { |
2f326e85 AE |
8350 | $element.click($.proxy(this._cancel, this)); |
8351 | } | |
453aced6 AE |
8352 | } |
8353 | }, this)); | |
8354 | }, | |
8355 | ||
2f326e85 AE |
8356 | /** |
8357 | * Cancels popovers if link is being clicked | |
8358 | */ | |
8359 | _cancel: function(event) { | |
8360 | this._cancelPopover = true; | |
8361 | this._hide(true); | |
8362 | }, | |
8363 | ||
453aced6 AE |
8364 | /** |
8365 | * Triggered once an element is being hovered. | |
8366 | * | |
8367 | * @param object event | |
8368 | */ | |
8369 | _overElement: function(event) { | |
2f326e85 AE |
8370 | if (this._cancelPopover) { |
8371 | return; | |
8372 | } | |
8373 | ||
453aced6 AE |
8374 | if (this._peOverElement !== null) { |
8375 | this._peOverElement.stop(); | |
8376 | } | |
8377 | ||
8378 | var $elementID = $(event.currentTarget).wcfIdentify(); | |
8379 | this._hoverElementID = $elementID; | |
8380 | this._peOverElement = new WCF.PeriodicalExecuter($.proxy(function(pe) { | |
8381 | pe.stop(); | |
8382 | ||
8383 | // still above the same element | |
8384 | if (this._hoverElementID === $elementID) { | |
8385 | this._activeElementID = $elementID; | |
8386 | this._prepare(); | |
8387 | } | |
8388 | }, this), this._delay.show); | |
8389 | ||
8390 | this._hoverElement = true; | |
8391 | this._hoverPopover = false; | |
8392 | }, | |
8393 | ||
8394 | /** | |
8395 | * Prepares popover to be displayed. | |
8396 | */ | |
8397 | _prepare: function() { | |
2f326e85 AE |
8398 | if (this._cancelPopover) { |
8399 | return; | |
8400 | } | |
8401 | ||
523908ab AE |
8402 | if (this._peOut !== null) { |
8403 | this._peOut.stop(); | |
8404 | } | |
8405 | ||
453aced6 AE |
8406 | // hide and reset |
8407 | if (this._popover.is(':visible')) { | |
5b755307 | 8408 | this._hide(true); |
453aced6 AE |
8409 | } |
8410 | ||
8411 | // insert html | |
8412 | if (!this._data[this._activeElementID].loading && this._data[this._activeElementID].content) { | |
5b755307 | 8413 | this._popoverContent.html(this._data[this._activeElementID].content); |
389d57cd | 8414 | |
42d7d2cc | 8415 | WCF.DOMNodeInsertedHandler.execute(); |
453aced6 AE |
8416 | } |
8417 | else { | |
453aced6 AE |
8418 | this._data[this._activeElementID].loading = true; |
8419 | } | |
8420 | ||
8421 | // get dimensions | |
8422 | var $dimensions = this._popover.show().getDimensions(); | |
8423 | if (this._data[this._activeElementID].loading) { | |
8424 | $dimensions = { | |
8425 | height: Math.max($dimensions.height, this._defaultDimensions.height), | |
8426 | width: Math.max($dimensions.width, this._defaultDimensions.width) | |
8427 | }; | |
8428 | } | |
523908ab AE |
8429 | else { |
8430 | $dimensions = this._fixElementDimensions(this._popover, $dimensions); | |
8431 | } | |
453aced6 AE |
8432 | this._popover.hide(); |
8433 | ||
8434 | // get orientation | |
8435 | var $orientation = this._getOrientation($dimensions.height, $dimensions.width); | |
8436 | this._popover.css(this._getCSS($orientation.x, $orientation.y)); | |
8437 | ||
379272f7 AE |
8438 | // apply orientation to popover |
8439 | this._popover.removeClass('bottom left right top').addClass($orientation.x).addClass($orientation.y); | |
8440 | ||
453aced6 AE |
8441 | this._show(); |
8442 | }, | |
8443 | ||
8444 | /** | |
8445 | * Displays the popover. | |
8446 | */ | |
8447 | _show: function() { | |
2f326e85 AE |
8448 | if (this._cancelPopover) { |
8449 | return; | |
8450 | } | |
8451 | ||
57de5168 | 8452 | this._popover.stop().show().css({ opacity: 1 }).wcfFadeIn(); |
453aced6 | 8453 | |
1e5b5b23 | 8454 | if (this._data[this._activeElementID].loading) { |
556973c1 | 8455 | this._popover.children('span').show(); |
1e5b5b23 AE |
8456 | this._loadContent(); |
8457 | } | |
5b755307 | 8458 | else { |
556973c1 | 8459 | this._popover.children('span').hide(); |
5b755307 AE |
8460 | this._popoverContent.css({ opacity: 1 }); |
8461 | } | |
453aced6 AE |
8462 | }, |
8463 | ||
8464 | /** | |
8465 | * Loads content, should be overwritten by child classes. | |
8466 | */ | |
8467 | _loadContent: function() { }, | |
8468 | ||
379272f7 AE |
8469 | /** |
8470 | * Inserts content and animating transition. | |
8471 | * | |
8472 | * @param string elementID | |
8473 | * @param boolean animate | |
8474 | */ | |
8475 | _insertContent: function(elementID, content, animate) { | |
8476 | this._data[elementID] = { | |
8477 | content: content, | |
8478 | loading: false | |
8479 | }; | |
8480 | ||
8481 | // only update content if element id is active | |
8482 | if (this._activeElementID === elementID) { | |
1e5b5b23 AE |
8483 | if (animate) { |
8484 | // get current dimensions | |
5b755307 | 8485 | var $dimensions = this._popoverContent.getDimensions(); |
1e5b5b23 AE |
8486 | |
8487 | // insert new content | |
603cfb39 AE |
8488 | this._popoverContent.css({ |
8489 | height: 'auto', | |
8490 | width: 'auto' | |
8491 | }); | |
5b755307 AE |
8492 | this._popoverContent.html(this._data[elementID].content); |
8493 | var $newDimensions = this._popoverContent.getDimensions(); | |
1e5b5b23 AE |
8494 | |
8495 | // enforce current dimensions and remove HTML | |
5b755307 | 8496 | this._popoverContent.html('').css({ |
1e5b5b23 AE |
8497 | height: $dimensions.height + 'px', |
8498 | width: $dimensions.width + 'px' | |
8499 | }); | |
8500 | ||
8501 | // animate to new dimensons | |
5b755307 AE |
8502 | var self = this; |
8503 | this._popoverContent.animate({ | |
1e5b5b23 AE |
8504 | height: $newDimensions.height + 'px', |
8505 | width: $newDimensions.width + 'px' | |
8506 | }, 300, function() { | |
556973c1 | 8507 | self._popover.children('span').hide(); |
5b755307 | 8508 | self._popoverContent.html(self._data[elementID].content).css({ opacity: 0 }).animate({ opacity: 1 }, 200); |
603cfb39 | 8509 | |
42d7d2cc | 8510 | WCF.DOMNodeInsertedHandler.execute(); |
1e5b5b23 AE |
8511 | }); |
8512 | } | |
8513 | else { | |
8514 | // insert new content | |
556973c1 | 8515 | this._popover.children('span').hide(); |
5b755307 | 8516 | this._popoverContent.html(this._data[elementID].content); |
42d7d2cc AE |
8517 | |
8518 | WCF.DOMNodeInsertedHandler.execute(); | |
1e5b5b23 | 8519 | } |
379272f7 AE |
8520 | } |
8521 | }, | |
8522 | ||
453aced6 AE |
8523 | /** |
8524 | * Hides the popover. | |
8525 | */ | |
5b755307 AE |
8526 | _hide: function(disableAnimation) { |
8527 | var self = this; | |
8528 | this._popoverContent.stop(); | |
8529 | this._popover.stop(); | |
8530 | ||
8531 | if (disableAnimation) { | |
57de5168 | 8532 | self._popover.css({ opacity: 0 }).hide(); |
5b755307 AE |
8533 | self._popoverContent.empty().css({ height: 'auto', opacity: 0, width: 'auto' }); |
8534 | } | |
8535 | else { | |
8536 | this._popover.wcfFadeOut(function() { | |
fc821303 AE |
8537 | self._popoverContent.empty().css({ height: 'auto', opacity: 0, width: 'auto' }); |
8538 | self._popover.hide(); | |
5b755307 AE |
8539 | }); |
8540 | } | |
453aced6 AE |
8541 | }, |
8542 | ||
8543 | /** | |
8544 | * Triggered once popover is being hovered. | |
8545 | */ | |
8546 | _overPopover: function() { | |
523908ab AE |
8547 | if (this._peOut !== null) { |
8548 | this._peOut.stop(); | |
8549 | } | |
8550 | ||
453aced6 AE |
8551 | this._hoverElement = false; |
8552 | this._hoverPopover = true; | |
8553 | }, | |
8554 | ||
8555 | /** | |
8556 | * Triggered once element *or* popover is now longer hovered. | |
8557 | */ | |
8558 | _out: function(event) { | |
2f326e85 AE |
8559 | if (this._cancelPopover) { |
8560 | return; | |
8561 | } | |
8562 | ||
1e5b5b23 | 8563 | this._hoverElementID = ''; |
453aced6 AE |
8564 | this._hoverElement = false; |
8565 | this._hoverPopover = false; | |
8566 | ||
523908ab | 8567 | this._peOut = new WCF.PeriodicalExecuter($.proxy(function(pe) { |
453aced6 AE |
8568 | pe.stop(); |
8569 | ||
8570 | // hide popover is neither element nor popover was hovered given time | |
8571 | if (!this._hoverElement && !this._hoverPopover) { | |
5b755307 | 8572 | this._hide(false); |
453aced6 AE |
8573 | } |
8574 | }, this), this._delay.hide); | |
8575 | }, | |
8576 | ||
8577 | /** | |
8578 | * Resolves popover orientation, tries to use default orientation first. | |
8579 | * | |
8580 | * @param integer height | |
8581 | * @param integer width | |
8582 | * @return object | |
8583 | */ | |
8584 | _getOrientation: function(height, width) { | |
8585 | // get offsets and dimensions | |
8586 | var $element = $('#' + this._activeElementID); | |
523908ab | 8587 | var $offsets = $element.getOffsets('offset'); |
453aced6 AE |
8588 | var $elementDimensions = $element.getDimensions(); |
8589 | var $documentDimensions = $(document).getDimensions(); | |
8590 | ||
8591 | // try default orientation first | |
8592 | var $orientationX = (this._defaultOrientation.x === 'left') ? 'left' : 'right'; | |
8593 | var $orientationY = (this._defaultOrientation.y === 'bottom') ? 'bottom' : 'top'; | |
379272f7 | 8594 | var $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width); |
453aced6 AE |
8595 | |
8596 | if ($result.flawed) { | |
8597 | // try flipping orientationX | |
8598 | $orientationX = ($orientationX === 'left') ? 'right' : 'left'; | |
8599 | $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width); | |
8600 | ||
8601 | if ($result.flawed) { | |
8602 | // try flipping orientationY while maintaing original orientationX | |
8603 | $orientationX = ($orientationX === 'right') ? 'left' : 'right'; | |
8604 | $orientationY = ($orientationY === 'bottom') ? 'top' : 'bottom'; | |
8605 | $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width); | |
8606 | ||
8607 | if ($result.flawed) { | |
8608 | // try flipping both orientationX and orientationY compared to default values | |
8609 | $orientationX = ($orientationX === 'left') ? 'right' : 'left'; | |
8610 | $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width); | |
8611 | ||
8612 | if ($result.flawed) { | |
8613 | // fuck this shit, we will use the default orientation | |
8614 | $orientationX = (this._defaultOrientationX === 'left') ? 'left' : 'right'; | |
8615 | $orientationY = (this._defaultOrientationY === 'bottom') ? 'bottom' : 'top'; | |
8616 | } | |
8617 | } | |
8618 | } | |
8619 | } | |
8620 | ||
8621 | return { | |
8622 | x: $orientationX, | |
8623 | y: $orientationY | |
d27e2667 | 8624 | }; |
453aced6 AE |
8625 | }, |
8626 | ||
8627 | /** | |
8628 | * Evaluates if popover fits into given orientation. | |
8629 | * | |
8630 | * @param string orientationX | |
8631 | * @param string orientationY | |
8632 | * @param object offsets | |
8633 | * @param object elementDimensions | |
8634 | * @param object documentDimensions | |
8635 | * @param integer height | |
8636 | * @param integer width | |
8637 | * @return object | |
8638 | */ | |
8639 | _evaluateOrientation: function(orientationX, orientationY, offsets, elementDimensions, documentDimensions, height, width) { | |
8640 | var $heightDifference = 0, $widthDifference = 0; | |
8641 | switch (orientationX) { | |
8642 | case 'left': | |
8643 | $widthDifference = offsets.left - width; | |
8644 | break; | |
8645 | ||
8646 | case 'right': | |
523908ab | 8647 | $widthDifference = documentDimensions.width - (offsets.left + width); |
453aced6 AE |
8648 | break; |
8649 | } | |
8650 | ||
8651 | switch (orientationY) { | |
8652 | case 'bottom': | |
523908ab | 8653 | $heightDifference = documentDimensions.height - (offsets.top + elementDimensions.height + this._popoverOffset + height); |
453aced6 AE |
8654 | break; |
8655 | ||
8656 | case 'top': | |
8657 | $heightDifference = offsets.top - (height - this._popoverOffset); | |
8658 | break; | |
8659 | } | |
8660 | ||
8661 | // check if both difference are above margin | |
8662 | var $flawed = false; | |
8663 | if ($heightDifference < this._margin || $widthDifference < this._margin) { | |
8664 | $flawed = true; | |
8665 | } | |
8666 | ||
8667 | return { | |
8668 | flawed: $flawed, | |
8669 | x: $widthDifference, | |
8670 | y: $heightDifference | |
8671 | }; | |
8672 | }, | |
8673 | ||
8674 | /** | |
8675 | * Computes CSS for popover. | |
8676 | * | |
8677 | * @param string orientationX | |
8678 | * @param string orientationY | |
8679 | * @return object | |
8680 | */ | |
8681 | _getCSS: function(orientationX, orientationY) { | |
8682 | var $css = { | |
8683 | bottom: 'auto', | |
8684 | left: 'auto', | |
453aced6 AE |
8685 | right: 'auto', |
8686 | top: 'auto' | |
8687 | }; | |
8688 | ||
8689 | var $element = $('#' + this._activeElementID); | |
523908ab AE |
8690 | var $offsets = $element.getOffsets('offset'); |
8691 | var $elementDimensions = this._fixElementDimensions($element, $element.getDimensions()); | |
379272f7 | 8692 | var $windowDimensions = $(window).getDimensions(); |
453aced6 AE |
8693 | |
8694 | switch (orientationX) { | |
8695 | case 'left': | |
379272f7 | 8696 | $css.right = $windowDimensions.width - ($offsets.left + $elementDimensions.width); |
453aced6 AE |
8697 | break; |
8698 | ||
8699 | case 'right': | |
8700 | $css.left = $offsets.left; | |
8701 | break; | |
8702 | } | |
8703 | ||
8704 | switch (orientationY) { | |
8705 | case 'bottom': | |
8706 | $css.top = $offsets.top + ($elementDimensions.height + this._popoverOffset); | |
8707 | break; | |
8708 | ||
8709 | case 'top': | |
379272f7 | 8710 | $css.bottom = $windowDimensions.height - ($offsets.top - this._popoverOffset); |
453aced6 AE |
8711 | break; |
8712 | } | |
8713 | ||
8714 | return $css; | |
523908ab AE |
8715 | }, |
8716 | ||
8717 | /** | |
8718 | * Tries to fix dimensions if element is partially hidden (overflow: hidden). | |
8719 | * | |
8720 | * @param jQuery element | |
8721 | * @param object dimensions | |
8722 | * @return dimensions | |
8723 | */ | |
8724 | _fixElementDimensions: function(element, dimensions) { | |
8725 | var $parentDimensions = element.parent().getDimensions(); | |
8726 | ||
8727 | if ($parentDimensions.height < dimensions.height) { | |
8728 | dimensions.height = $parentDimensions.height; | |
8729 | } | |
8730 | ||
8731 | if ($parentDimensions.width < dimensions.width) { | |
8732 | dimensions.width = $parentDimensions.width; | |
8733 | } | |
8734 | ||
8735 | return dimensions; | |
453aced6 AE |
8736 | } |
8737 | }); | |
8738 | ||
cede0aec AE |
8739 | /** |
8740 | * Provides an extensible item list with built-in search. | |
8741 | * | |
8742 | * @param string itemListSelector | |
8743 | * @param string searchInputSelector | |
8744 | */ | |
d27e2667 | 8745 | WCF.EditableItemList = Class.extend({ |
cede0aec AE |
8746 | /** |
8747 | * allows custom input not recognized by search to be added | |
8748 | * @var boolean | |
8749 | */ | |
8750 | _allowCustomInput: false, | |
8751 | ||
8752 | /** | |
8753 | * action class name | |
8754 | * @var string | |
8755 | */ | |
8756 | _className: '', | |
8757 | ||
8758 | /** | |
8759 | * internal data storage | |
8760 | * @var mixed | |
8761 | */ | |
d27e2667 | 8762 | _data: { }, |
cede0aec AE |
8763 | |
8764 | /** | |
8765 | * form container | |
8766 | * @var jQuery | |
8767 | */ | |
8768 | _form: null, | |
8769 | ||
8770 | /** | |
8771 | * item list container | |
8772 | * @var jQuery | |
8773 | */ | |
d27e2667 | 8774 | _itemList: null, |
cede0aec AE |
8775 | |
8776 | /** | |
8777 | * current object id | |
8778 | * @var integer | |
8779 | */ | |
8780 | _objectID: 0, | |
8781 | ||
8782 | /** | |
8783 | * object type id | |
8784 | * @var integer | |
8785 | */ | |
8786 | _objectTypeID: 0, | |
8787 | ||
8788 | /** | |
8789 | * search controller | |
8790 | * @var WCF.Search.Base | |
8791 | */ | |
d27e2667 | 8792 | _search: null, |
cede0aec AE |
8793 | |
8794 | /** | |
8795 | * search input element | |
8796 | * @var jQuery | |
8797 | */ | |
d27e2667 AE |
8798 | _searchInput: null, |
8799 | ||
cede0aec AE |
8800 | /** |
8801 | * Creates a new WCF.EditableItemList object. | |
8802 | * | |
8803 | * @param string itemListSelector | |
8804 | * @param string searchInputSelector | |
8805 | */ | |
d27e2667 AE |
8806 | init: function(itemListSelector, searchInputSelector) { |
8807 | this._itemList = $(itemListSelector); | |
8808 | this._searchInput = $(searchInputSelector); | |
0579855e | 8809 | this._data = { }; |
d27e2667 AE |
8810 | |
8811 | if (!this._itemList.length || !this._searchInput.length) { | |
8812 | console.debug("[WCF.EditableItemList] Item list and/or search input do not exist, aborting."); | |
8813 | return; | |
8814 | } | |
8815 | ||
cede0aec AE |
8816 | this._objectID = this._getObjectID(); |
8817 | this._objectTypeID = this._getObjectTypeID(); | |
8818 | ||
d27e2667 AE |
8819 | // bind item listener |
8820 | this._itemList.find('.jsEditableItem').click($.proxy(this._click, this)); | |
cede0aec AE |
8821 | |
8822 | // create item list | |
8823 | if (!this._itemList.children('ul').length) { | |
8824 | $('<ul />').appendTo(this._itemList); | |
8825 | } | |
8826 | this._itemList = this._itemList.children('ul'); | |
8827 | ||
8828 | // bind form submit | |
8829 | this._form = this._itemList.parents('form').submit($.proxy(this._submit, this)); | |
8830 | ||
8831 | if (this._allowCustomInput) { | |
3289653d AE |
8832 | var self = this; |
8833 | this._searchInput.keydown($.proxy(this._keyDown, this)).on('paste', function() { | |
8834 | setTimeout(function() { self._onPaste(); }, 100); | |
8835 | }); | |
80da9de3 | 8836 | } |
7c6f7523 AE |
8837 | |
8838 | // block form submit through [ENTER] | |
8839 | this._searchInput.parents('.dropdown').data('preventSubmit', true); | |
80da9de3 AE |
8840 | }, |
8841 | ||
8842 | /** | |
8843 | * Handles the key down event. | |
8844 | * | |
8845 | * @param object event | |
8846 | */ | |
8847 | _keyDown: function(event) { | |
e3423595 | 8848 | // 188 = [,] |
7c6f7523 | 8849 | if (event === null || event.which === 188 || event.which === $.ui.keyCode.ENTER) { |
6aa97133 | 8850 | if (event !== null && event.which === $.ui.keyCode.ENTER && this._search) { |
7c6f7523 AE |
8851 | if (this._search._itemIndex !== -1) { |
8852 | return false; | |
8853 | } | |
8854 | } | |
8855 | ||
80da9de3 | 8856 | var $value = $.trim(this._searchInput.val()); |
e3423595 AE |
8857 | |
8858 | // read everything left from caret position | |
928f1a86 | 8859 | if (event && event.which === 188) { |
e3423595 AE |
8860 | $value = $value.substring(0, this._searchInput.getCaret()); |
8861 | } | |
8862 | ||
80da9de3 | 8863 | if ($value === '') { |
cede0aec | 8864 | return true; |
80da9de3 AE |
8865 | } |
8866 | ||
8867 | this.addItem({ | |
8868 | objectID: 0, | |
8869 | label: $value | |
cede0aec | 8870 | }); |
80da9de3 AE |
8871 | |
8872 | // reset input | |
928f1a86 | 8873 | if (event && event.which === 188) { |
e3423595 AE |
8874 | this._searchInput.val($.trim(this._searchInput.val().substr(this._searchInput.getCaret()))); |
8875 | } | |
8876 | else { | |
8877 | this._searchInput.val(''); | |
8878 | } | |
80da9de3 AE |
8879 | |
8880 | if (event !== null) { | |
8881 | event.stopPropagation(); | |
8882 | } | |
8883 | ||
8884 | return false; | |
cede0aec | 8885 | } |
80da9de3 AE |
8886 | |
8887 | return true; | |
d27e2667 AE |
8888 | }, |
8889 | ||
3289653d AE |
8890 | /** |
8891 | * Handle paste event. | |
8892 | */ | |
8893 | _onPaste: function() { | |
8894 | // split content by comma | |
8895 | var $value = $.trim(this._searchInput.val()); | |
8896 | $value = $value.split(','); | |
8897 | ||
8898 | for (var $i = 0, $length = $value.length; $i < $length; $i++) { | |
8899 | var $label = $.trim($value[$i]); | |
8900 | if ($label === '') { | |
8901 | continue; | |
8902 | } | |
8903 | ||
8904 | this.addItem({ | |
8905 | objectID: 0, | |
8906 | label: $label | |
8907 | }); | |
8908 | } | |
8909 | ||
8910 | this._searchInput.val(''); | |
8911 | }, | |
8912 | ||
d27e2667 AE |
8913 | /** |
8914 | * Loads raw data and converts it into internal structure. Override this methods | |
8915 | * in your derived classes. | |
8916 | * | |
8917 | * @param object data | |
8918 | */ | |
8919 | load: function(data) { }, | |
8920 | ||
cede0aec AE |
8921 | /** |
8922 | * Removes an item on click. | |
8923 | * | |
8924 | * @param object event | |
8925 | * @return boolean | |
8926 | */ | |
d27e2667 AE |
8927 | _click: function(event) { |
8928 | var $element = $(event.currentTarget); | |
8929 | var $objectID = $element.data('objectID'); | |
cede0aec | 8930 | var $label = $element.data('label'); |
d27e2667 | 8931 | |
0b9520f4 MS |
8932 | if (this._search) { |
8933 | this._search.removeExcludedSearchValue($label); | |
8934 | } | |
cede0aec | 8935 | this._removeItem($objectID, $label); |
d27e2667 AE |
8936 | |
8937 | $element.remove(); | |
8938 | ||
8939 | event.stopPropagation(); | |
8940 | return false; | |
8941 | }, | |
8942 | ||
cede0aec AE |
8943 | /** |
8944 | * Returns current object id. | |
8945 | * | |
8946 | * @return integer | |
8947 | */ | |
8948 | _getObjectID: function() { | |
8949 | return 0; | |
8950 | }, | |
8951 | ||
8952 | /** | |
8953 | * Returns current object type id. | |
8954 | * | |
8955 | * @return integer | |
8956 | */ | |
8957 | _getObjectTypeID: function() { | |
8958 | return 0; | |
8959 | }, | |
8960 | ||
8961 | /** | |
8962 | * Adds a new item to the list. | |
8963 | * | |
8964 | * @param object data | |
8965 | * @return boolean | |
8966 | */ | |
8967 | addItem: function(data) { | |
8968 | if (this._data[data.objectID]) { | |
8717050f AE |
8969 | if (!(data.objectID === 0 && this._allowCustomInput)) { |
8970 | return true; | |
8971 | } | |
cede0aec AE |
8972 | } |
8973 | ||
e5f1745c | 8974 | var $listItem = $('<li class="badge">' + WCF.String.escapeHTML(data.label) + '</li>').data('objectID', data.objectID).data('label', data.label).appendTo(this._itemList); |
cede0aec | 8975 | $listItem.click($.proxy(this._click, this)); |
d27e2667 | 8976 | |
0b9520f4 MS |
8977 | if (this._search) { |
8978 | this._search.addExcludedSearchValue(data.label); | |
8979 | } | |
cede0aec AE |
8980 | this._addItem(data.objectID, data.label); |
8981 | ||
8982 | return true; | |
8983 | }, | |
8984 | ||
4d28f1de MS |
8985 | /** |
8986 | * Clears the list of items. | |
8987 | */ | |
8988 | clearList: function() { | |
8989 | this._itemList.children('li').each($.proxy(function(index, element) { | |
8990 | var $element = $(element); | |
8991 | ||
8992 | if (this._search) { | |
8993 | this._search.removeExcludedSearchValue($element.data('label')); | |
8994 | } | |
8995 | ||
8996 | $element.remove(); | |
8997 | this._removeItem($element.data('objectID'), $element.data('label')); | |
8998 | }, this)); | |
8999 | }, | |
9000 | ||
cede0aec AE |
9001 | /** |
9002 | * Handles form submit, override in your class. | |
9003 | */ | |
80da9de3 AE |
9004 | _submit: function() { |
9005 | this._keyDown(null); | |
9006 | }, | |
cede0aec AE |
9007 | |
9008 | /** | |
9009 | * Adds an item to internal storage. | |
9010 | * | |
9011 | * @param integer objectID | |
9012 | * @param string label | |
9013 | */ | |
9014 | _addItem: function(objectID, label) { | |
9015 | this._data[objectID] = label; | |
9016 | }, | |
9017 | ||
9018 | /** | |
9019 | * Removes an item from internal storage. | |
9020 | * | |
9021 | * @param integer objectID | |
9022 | * @param string label | |
9023 | */ | |
9024 | _removeItem: function(objectID, label) { | |
9025 | delete this._data[objectID]; | |
4d28f1de MS |
9026 | }, |
9027 | ||
9028 | /** | |
9029 | * Returns the search input field. | |
9030 | * | |
9031 | * @return jQuery | |
9032 | */ | |
9033 | getSearchInput: function() { | |
9034 | return this._searchInput; | |
d27e2667 AE |
9035 | } |
9036 | }); | |
9037 | ||
27c3b95f AE |
9038 | /** |
9039 | * Provides a generic sitemap. | |
9040 | */ | |
9041 | WCF.Sitemap = Class.extend({ | |
9042 | /** | |
9043 | * sitemap name cache | |
9f959ced | 9044 | * @var array |
27c3b95f AE |
9045 | */ |
9046 | _cache: [ ], | |
9047 | ||
9048 | /** | |
9049 | * dialog overlay | |
9050 | * @var jQuery | |
9051 | */ | |
9052 | _dialog: null, | |
9053 | ||
9054 | /** | |
9055 | * initialization state | |
9056 | * @var boolean | |
9057 | */ | |
9058 | _didInit: false, | |
9059 | ||
9060 | /** | |
9061 | * action proxy | |
9062 | * @var WCF.Action.Proxy | |
9063 | */ | |
9064 | _proxy: null, | |
9065 | ||
9066 | /** | |
9067 | * Initializes the generic sitemap. | |
9068 | */ | |
9069 | init: function() { | |
9070 | $('#sitemap').click($.proxy(this._click, this)); | |
9071 | ||
9072 | this._cache = [ ]; | |
9073 | this._dialog = null; | |
9074 | this._didInit = false; | |
9075 | this._proxy = new WCF.Action.Proxy({ | |
9076 | success: $.proxy(this._success, this) | |
9077 | }); | |
9078 | }, | |
9079 | ||
9080 | /** | |
9081 | * Handles clicks on the sitemap icon. | |
9082 | */ | |
9083 | _click: function() { | |
9084 | if (this._dialog === null) { | |
9085 | this._dialog = $('<div id="sitemapDialog" />').appendTo(document.body); | |
9086 | ||
9087 | this._proxy.setOption('data', { | |
9088 | actionName: 'getSitemap', | |
9089 | className: 'wcf\\data\\sitemap\\SitemapAction' | |
9090 | }); | |
9091 | this._proxy.sendRequest(); | |
9092 | } | |
9093 | else { | |
9094 | this._dialog.wcfDialog('open'); | |
9095 | } | |
9096 | }, | |
9097 | ||
9098 | /** | |
9099 | * Handles successful AJAX responses. | |
9100 | * | |
9101 | * @param object data | |
9102 | * @param string textStatus | |
9103 | * @param jQuery jqXHR | |
9104 | */ | |
9105 | _success: function(data, textStatus, jqXHR) { | |
9106 | if (this._didInit) { | |
9107 | this._cache.push(data.returnValues.sitemapName); | |
9108 | ||
47f83a32 | 9109 | this._dialog.find('#sitemap_' + data.returnValues.sitemapName).html(data.returnValues.template); |
27c3b95f AE |
9110 | |
9111 | // redraw dialog | |
9112 | this._dialog.wcfDialog('render'); | |
9113 | } | |
9114 | else { | |
9115 | // mark sitemap name as loaded | |
9116 | this._cache.push(data.returnValues.sitemapName); | |
9117 | ||
9118 | // insert sitemap template | |
9119 | this._dialog.html(data.returnValues.template); | |
9120 | ||
9121 | // bind event listener | |
9122 | this._dialog.find('.sitemapNavigation').click($.proxy(this._navigate, this)); | |
9123 | ||
7c3a12b1 AE |
9124 | // select active item |
9125 | this._dialog.find('.tabMenuContainer').wcfTabs('select', 'sitemap_' + data.returnValues.sitemapName); | |
9126 | ||
27c3b95f AE |
9127 | // show dialog |
9128 | this._dialog.wcfDialog({ | |
5c2036ab | 9129 | title: WCF.Language.get('wcf.page.sitemap') |
27c3b95f AE |
9130 | }); |
9131 | ||
9132 | this._didInit = true; | |
9133 | } | |
9134 | }, | |
9135 | ||
9136 | /** | |
9137 | * Navigates between different sitemaps. | |
9138 | * | |
9139 | * @param object event | |
9140 | */ | |
9141 | _navigate: function(event) { | |
9142 | var $sitemapName = $(event.currentTarget).data('sitemapName'); | |
9143 | if (WCF.inArray($sitemapName, this._cache)) { | |
47f83a32 | 9144 | this._dialog.find('.tabMenuContainer').wcfTabs('select', 'sitemap_' + $sitemapName); |
27c3b95f AE |
9145 | |
9146 | // redraw dialog | |
9147 | this._dialog.wcfDialog('render'); | |
9148 | } | |
9149 | else { | |
9150 | this._proxy.setOption('data', { | |
9151 | actionName: 'getSitemap', | |
9152 | className: 'wcf\\data\\sitemap\\SitemapAction', | |
9153 | parameters: { | |
9154 | sitemapName: $sitemapName | |
9155 | } | |
9156 | }); | |
9157 | this._proxy.sendRequest(); | |
9158 | } | |
9159 | } | |
9160 | }); | |
9161 | ||
a5423278 AE |
9162 | /** |
9163 | * Provides a language chooser. | |
9164 | * | |
9165 | * @param string containerID | |
9166 | * @param string inputFieldID | |
9167 | * @param integer languageID | |
9168 | * @param object languages | |
9169 | * @param object callback | |
9170 | */ | |
9171 | WCF.Language.Chooser = Class.extend({ | |
9172 | /** | |
9173 | * callback object | |
9174 | * @var object | |
9175 | */ | |
9176 | _callback: null, | |
9177 | ||
9178 | /** | |
9179 | * dropdown object | |
9180 | * @var jQuery | |
9181 | */ | |
9182 | _dropdown: null, | |
9183 | ||
9184 | /** | |
9185 | * input field | |
9186 | * @var jQuery | |
9187 | */ | |
9188 | _input: null, | |
9189 | ||
9190 | /** | |
9191 | * Initializes the language chooser. | |
9192 | * | |
9193 | * @param string containerID | |
9194 | * @param string inputFieldID | |
9195 | * @param integer languageID | |
9196 | * @param object languages | |
9197 | * @param object callback | |
65b1d6a0 | 9198 | * @param boolean allowEmptyValue |
a5423278 | 9199 | */ |
65b1d6a0 | 9200 | init: function(containerID, inputFieldID, languageID, languages, callback, allowEmptyValue) { |
a5423278 AE |
9201 | var $container = $('#' + containerID); |
9202 | if ($container.length != 1) { | |
9203 | console.debug("[WCF.Language.Chooser] Invalid container id '" + containerID + "' given"); | |
9204 | return; | |
9205 | } | |
9206 | ||
9207 | // bind language id input | |
9208 | this._input = $('#' + inputFieldID); | |
9209 | if (!this._input.length) { | |
9210 | this._input = $('<input type="hidden" name="' + inputFieldID + '" value="' + languageID + '" />').appendTo($container); | |
9211 | } | |
9212 | ||
9213 | // handle callback | |
9214 | if (callback !== undefined) { | |
9215 | if (!$.isFunction(callback)) { | |
9216 | console.debug("[WCF.Language.Chooser] Given callback is invalid"); | |
9217 | return; | |
9218 | } | |
9219 | ||
9220 | this._callback = callback; | |
9221 | } | |
9222 | ||
9223 | // create language dropdown | |
9224 | this._dropdown = $('<div class="dropdown" id="' + containerID + '-languageChooser" />').appendTo($container); | |
f3e301ca | 9225 | $('<div class="dropdownToggle boxFlag box24" data-toggle="' + containerID + '-languageChooser"></div>').appendTo(this._dropdown); |
a5423278 AE |
9226 | var $dropdownMenu = $('<ul class="dropdownMenu" />').appendTo(this._dropdown); |
9227 | ||
9228 | for (var $languageID in languages) { | |
9229 | var $language = languages[$languageID]; | |
635a8feb | 9230 | var $item = $('<li class="boxFlag"><a class="box24"><div class="framed"><img src="' + $language.iconPath + '" alt="" class="iconFlag" /></div> <div><h3>' + $language.languageName + '</h3></div></a></li>').appendTo($dropdownMenu); |
a5423278 AE |
9231 | $item.data('languageID', $languageID).click($.proxy(this._click, this)); |
9232 | ||
9233 | // update dropdown label | |
9234 | if ($languageID == languageID) { | |
f3e301ca MW |
9235 | var $html = $('' + $item.html()); |
9236 | var $innerContent = $html.children().detach(); | |
9237 | this._dropdown.children('.dropdownToggle').empty().append($innerContent); | |
a5423278 AE |
9238 | } |
9239 | } | |
9240 | ||
65b1d6a0 AE |
9241 | // allow an empty selection (e.g. using as language filter) |
9242 | if (allowEmptyValue) { | |
9243 | $('<li class="dropdownDivider" />').appendTo($dropdownMenu); | |
9244 | var $item = $('<li><a>' + WCF.Language.get('wcf.global.language.noSelection') + '</a></li>').data('languageID', 0).click($.proxy(this._click, this)).appendTo($dropdownMenu); | |
9245 | ||
9246 | if (languageID === 0) { | |
9247 | this._dropdown.children('.dropdownToggle').empty().append($item.html()); | |
9248 | } | |
9249 | } | |
9250 | ||
a5423278 AE |
9251 | WCF.Dropdown.init(); |
9252 | }, | |
9253 | ||
9254 | /** | |
9255 | * Handles click events. | |
9256 | * | |
9257 | * @param object event | |
9258 | */ | |
9259 | _click: function(event) { | |
9260 | var $item = $(event.currentTarget); | |
65b1d6a0 | 9261 | var $languageID = $item.data('languageID'); |
a5423278 AE |
9262 | |
9263 | // update input field | |
65b1d6a0 | 9264 | this._input.val($languageID); |
a5423278 AE |
9265 | |
9266 | // update dropdown label | |
f3e301ca | 9267 | var $html = $('' + $item.html()); |
65b1d6a0 | 9268 | var $innerContent = ($languageID === 0) ? $html : $html.children().detach(); |
f3e301ca | 9269 | this._dropdown.children('.dropdownToggle').empty().append($innerContent); |
a5423278 AE |
9270 | |
9271 | // execute callback | |
9272 | if (this._callback !== null) { | |
9273 | this._callback($item); | |
9274 | } | |
9275 | } | |
9276 | }); | |
9277 | ||
83736ee3 AE |
9278 | /** |
9279 | * Namespace for style related classes. | |
9280 | */ | |
9281 | WCF.Style = { }; | |
9282 | ||
9283 | /** | |
9284 | * Provides a visual style chooser. | |
9285 | */ | |
9286 | WCF.Style.Chooser = Class.extend({ | |
9287 | /** | |
9288 | * dialog overlay | |
9289 | * @var jQuery | |
9290 | */ | |
9291 | _dialog: null, | |
9292 | ||
9293 | /** | |
9294 | * action proxy | |
9295 | * @var WCF.Action.Proxy | |
9296 | */ | |
9297 | _proxy: null, | |
9298 | ||
9299 | /** | |
9300 | * Initializes the style chooser class. | |
9301 | */ | |
9302 | init: function() { | |
c275a0f3 | 9303 | $('<li class="styleChooser"><a>' + WCF.Language.get('wcf.style.changeStyle') + '</a></li>').appendTo($('#footerNavigation > ul.navigationItems')).click($.proxy(this._showDialog, this)); |
83736ee3 AE |
9304 | |
9305 | this._proxy = new WCF.Action.Proxy({ | |
9306 | success: $.proxy(this._success, this) | |
9307 | }); | |
9308 | }, | |
9309 | ||
9310 | /** | |
9311 | * Displays the style chooser dialog. | |
9312 | */ | |
9313 | _showDialog: function() { | |
9314 | if (this._dialog === null) { | |
9315 | this._dialog = $('<div id="styleChooser" />').hide().appendTo(document.body); | |
9316 | this._loadDialog(); | |
9317 | } | |
9318 | else { | |
9319 | this._dialog.wcfDialog({ | |
9320 | title: WCF.Language.get('wcf.style.changeStyle') | |
9321 | }); | |
9322 | } | |
9323 | }, | |
9324 | ||
9325 | /** | |
9326 | * Loads the style chooser dialog. | |
9327 | */ | |
9328 | _loadDialog: function() { | |
9329 | this._proxy.setOption('data', { | |
9330 | actionName: 'getStyleChooser', | |
9331 | className: 'wcf\\data\\style\\StyleAction' | |
9332 | }); | |
9333 | this._proxy.sendRequest(); | |
9334 | }, | |
9335 | ||
9336 | /** | |
9337 | * Handles successful AJAX requests. | |
9338 | * | |
9339 | * @param object data | |
9340 | * @param string textStatus | |
9341 | * @param jQuery jqXHR | |
9342 | */ | |
9343 | _success: function(data, textStatus, jqXHR) { | |
ab7de4fd | 9344 | if (data.actionName === 'changeStyle') { |
83736ee3 AE |
9345 | window.location.reload(); |
9346 | return; | |
9347 | } | |
9348 | ||
9349 | this._dialog.html(data.returnValues.template); | |
9350 | this._dialog.find('li').addClass('pointer').click($.proxy(this._click, this)); | |
9351 | ||
9352 | this._showDialog(); | |
9353 | }, | |
9354 | ||
9355 | /** | |
9356 | * Changes user style. | |
9357 | * | |
9358 | * @param object event | |
9359 | */ | |
9360 | _click: function(event) { | |
9361 | this._proxy.setOption('data', { | |
9362 | actionName: 'changeStyle', | |
9363 | className: 'wcf\\data\\style\\StyleAction', | |
9364 | objectIDs: [ $(event.currentTarget).data('styleID') ] | |
9365 | }); | |
9366 | this._proxy.sendRequest(); | |
9367 | } | |
9368 | }); | |
9369 | ||
271dbf28 AE |
9370 | /** |
9371 | * Converts static user panel items into interactive dropdowns. | |
9372 | * | |
9373 | * @param string containerID | |
9374 | */ | |
9375 | WCF.UserPanel = Class.extend({ | |
9376 | /** | |
9377 | * target container | |
9378 | * @var jQuery | |
9379 | */ | |
9380 | _container: null, | |
9381 | ||
9382 | /** | |
9383 | * initialization state | |
9384 | * @var boolean | |
9385 | */ | |
9386 | _didLoad: false, | |
9387 | ||
9388 | /** | |
9389 | * original link element | |
9390 | * @var jQuery | |
9391 | */ | |
9392 | _link: null, | |
9393 | ||
531e7fb1 AE |
9394 | /** |
9395 | * language variable name for 'no items' | |
9396 | * @var string | |
9397 | */ | |
9398 | _noItems: '', | |
9399 | ||
271dbf28 AE |
9400 | /** |
9401 | * reverts to original link if return values are empty | |
9402 | * @var boolean | |
9403 | */ | |
9404 | _revertOnEmpty: true, | |
9405 | ||
9406 | /** | |
9407 | * Initialites the WCF.UserPanel class. | |
9408 | * | |
9409 | * @param string containerID | |
9410 | */ | |
9411 | init: function(containerID) { | |
9412 | this._container = $('#' + containerID); | |
9413 | this._didLoad = false; | |
9414 | this._revertOnEmpty = true; | |
9415 | ||
9416 | if (this._container.length != 1) { | |
9417 | console.debug("[WCF.UserPanel] Unable to find container identfied by '" + containerID + "', aborting."); | |
9418 | return; | |
9419 | } | |
9420 | ||
531e7fb1 | 9421 | this._convert(); |
271dbf28 AE |
9422 | }, |
9423 | ||
9424 | /** | |
9425 | * Converts link into an interactive dropdown menu. | |
9426 | */ | |
9427 | _convert: function() { | |
271dbf28 AE |
9428 | this._container.addClass('dropdown'); |
9429 | this._link = this._container.children('a').remove(); | |
9430 | ||
3ef8dee9 | 9431 | var $button = $('<a class="dropdownToggle">' + this._link.html() + '</a>').appendTo(this._container).click($.proxy(this._click, this)); |
271dbf28 AE |
9432 | var $dropdownMenu = $('<ul class="dropdownMenu" />').appendTo(this._container); |
9433 | $('<li class="jsDropdownPlaceholder"><span>' + WCF.Language.get('wcf.global.loading') + '</span></li>').appendTo($dropdownMenu); | |
9434 | ||
9435 | this._addDefaultItems($dropdownMenu); | |
531e7fb1 AE |
9436 | |
9437 | this._container.dblclick($.proxy(function() { | |
9438 | window.location = this._link.attr('href'); | |
9439 | return false; | |
9440 | }, this)); | |
3ef8dee9 AE |
9441 | |
9442 | WCF.Dropdown.initDropdown($button, false); | |
271dbf28 AE |
9443 | }, |
9444 | ||
9445 | /** | |
9446 | * Adds default items to dropdown menu. | |
9447 | * | |
9448 | * @param jQuery dropdownMenu | |
9449 | */ | |
9450 | _addDefaultItems: function(dropdownMenu) { }, | |
9451 | ||
9452 | /** | |
9453 | * Adds a dropdown divider. | |
9454 | * | |
9455 | * @param jQuery dropdownMenu | |
9456 | */ | |
9457 | _addDivider: function(dropdownMenu) { | |
9458 | $('<li class="dropdownDivider" />').appendTo(dropdownMenu); | |
9459 | }, | |
9460 | ||
9461 | /** | |
9462 | * Handles clicks on the dropdown item. | |
9463 | */ | |
9464 | _click: function() { | |
9465 | if (this._didLoad) { | |
9466 | return; | |
9467 | } | |
9468 | ||
9469 | new WCF.Action.Proxy({ | |
9470 | autoSend: true, | |
9471 | data: this._getParameters(), | |
9472 | success: $.proxy(this._success, this) | |
9473 | }); | |
9474 | ||
9475 | this._didLoad = true; | |
9476 | }, | |
9477 | ||
9478 | /** | |
9479 | * Returns a list of parameters for AJAX request. | |
9480 | * | |
9481 | * @return object | |
9482 | */ | |
9483 | _getParameters: function() { | |
9484 | return { }; | |
9485 | }, | |
9486 | ||
9487 | /** | |
9488 | * Handles successful AJAX requests. | |
9489 | * | |
9490 | * @param object data | |
9491 | * @param string textStatus | |
9492 | * @param jQuery jqXHR | |
9493 | */ | |
9494 | _success: function(data, textStatus, jqXHR) { | |
3ef8dee9 | 9495 | var $dropdownMenu = WCF.Dropdown.getDropdownMenu(this._container.wcfIdentify()); |
531e7fb1 AE |
9496 | $dropdownMenu.children('.jsDropdownPlaceholder').remove(); |
9497 | ||
271dbf28 | 9498 | if (data.returnValues && data.returnValues.template) { |
271dbf28 | 9499 | $('' + data.returnValues.template).prependTo($dropdownMenu); |
5d10caf8 AE |
9500 | |
9501 | // update badge | |
9502 | var $badge = this._container.find('.badge'); | |
9503 | if (!$badge.length) { | |
9504 | $badge = $('<span class="badge badgeInverse" />').appendTo(this._container.children('.dropdownToggle')); | |
e6a05421 | 9505 | $badge.before(' '); |
5d10caf8 AE |
9506 | } |
9507 | $badge.html(data.returnValues.totalCount); | |
9508 | ||
1682f3cb | 9509 | this._after($dropdownMenu); |
271dbf28 AE |
9510 | } |
9511 | else { | |
531e7fb1 | 9512 | $('<li><span>' + WCF.Language.get(this._noItems) + '</span></li>').prependTo($dropdownMenu); |
271dbf28 AE |
9513 | |
9514 | // remove badge | |
9515 | this._container.find('.badge').remove(); | |
9516 | } | |
1682f3cb AE |
9517 | }, |
9518 | ||
9519 | /** | |
9520 | * Execute actions after the dropdown menu has been populated. | |
9521 | * | |
9522 | * @param object dropdownMenu | |
9523 | */ | |
9524 | _after: function(dropdownMenu) { } | |
271dbf28 AE |
9525 | }); |
9526 | ||
25763f41 AE |
9527 | /** |
9528 | * WCF implementation for dialogs, based upon ideas by jQuery UI. | |
9529 | */ | |
9530 | $.widget('ui.wcfDialog', { | |
9531 | /** | |
9532 | * close button | |
9533 | * @var jQuery | |
9534 | */ | |
9535 | _closeButton: null, | |
9f959ced | 9536 | |
25763f41 AE |
9537 | /** |
9538 | * dialog container | |
9539 | * @var jQuery | |
9540 | */ | |
9541 | _container: null, | |
9f959ced | 9542 | |
25763f41 AE |
9543 | /** |
9544 | * dialog content | |
9545 | * @var jQuery | |
9546 | */ | |
9547 | _content: null, | |
9f959ced | 9548 | |
25763f41 AE |
9549 | /** |
9550 | * modal overlay | |
9551 | * @var jQuery | |
9552 | */ | |
9553 | _overlay: null, | |
9f959ced | 9554 | |
25763f41 AE |
9555 | /** |
9556 | * plain html for title | |
9557 | * @var string | |
9558 | */ | |
9559 | _title: null, | |
9f959ced | 9560 | |
25763f41 AE |
9561 | /** |
9562 | * title bar | |
9563 | * @var jQuery | |
9564 | */ | |
9565 | _titlebar: null, | |
9f959ced | 9566 | |
25763f41 AE |
9567 | /** |
9568 | * dialog visibility state | |
9569 | * @var boolean | |
9570 | */ | |
9571 | _isOpen: false, | |
9f959ced | 9572 | |
25763f41 AE |
9573 | /** |
9574 | * option list | |
9575 | * @var object | |
9576 | */ | |
9577 | options: { | |
9578 | // dialog | |
9579 | autoOpen: true, | |
9580 | closable: true, | |
9581 | closeButtonLabel: null, | |
af7da802 AE |
9582 | closeConfirmMessage: null, |
9583 | closeViaModal: true, | |
25763f41 AE |
9584 | hideTitle: false, |
9585 | modal: true, | |
9586 | title: '', | |
184a8d6d | 9587 | zIndex: 400, |
9f959ced | 9588 | |
34e158d9 AE |
9589 | // event callbacks |
9590 | onClose: null, | |
9591 | onShow: null | |
25763f41 | 9592 | }, |
9f959ced | 9593 | |
38fc45af AE |
9594 | /** |
9595 | * @see $.widget._createWidget() | |
9596 | */ | |
9597 | _createWidget: function(options, element) { | |
9598 | // ignore script tags | |
9599 | if ($(element).getTagName() === 'script') { | |
9600 | console.debug("[ui.wcfDialog] Ignored script tag"); | |
bd16ec38 | 9601 | this.element = false; |
38fc45af AE |
9602 | return null; |
9603 | } | |
9604 | ||
9605 | $.Widget.prototype._createWidget.apply(this, arguments); | |
9606 | }, | |
9607 | ||
25763f41 AE |
9608 | /** |
9609 | * Initializes a new dialog. | |
9610 | */ | |
9611 | _init: function() { | |
25763f41 AE |
9612 | if (this.options.autoOpen) { |
9613 | this.open(); | |
9614 | } | |
9f959ced | 9615 | |
25763f41 | 9616 | // act on resize |
6e3d3bf1 | 9617 | $(window).resize($.proxy(this._resize, this)); |
25763f41 | 9618 | }, |
9f959ced | 9619 | |
25763f41 AE |
9620 | /** |
9621 | * Creates a new dialog instance. | |
9622 | */ | |
9623 | _create: function() { | |
4445e78f AE |
9624 | if (this.options.closeButtonLabel === null) { |
9625 | this.options.closeButtonLabel = WCF.Language.get('wcf.global.button.close'); | |
9626 | } | |
9627 | ||
25763f41 | 9628 | // create dialog container |
f74219d0 MK |
9629 | this._container = $('<div class="dialogContainer" />').hide().css({ zIndex: this.options.zIndex }).appendTo(document.body); |
9630 | this._titlebar = $('<header class="dialogTitlebar" />').hide().appendTo(this._container); | |
9631 | this._title = $('<span class="dialogTitle" />').hide().appendTo(this._titlebar); | |
4445e78f | 9632 | this._closeButton = $('<a class="dialogCloseButton jsTooltip" title="' + this.options.closeButtonLabel + '"><span /></a>').click($.proxy(this.close, this)).hide().appendTo(this._titlebar); |
f74219d0 | 9633 | this._content = $('<div class="dialogContent" />').appendTo(this._container); |
25763f41 | 9634 | |
f74219d0 MK |
9635 | this._setOption('title', this.options.title); |
9636 | this._setOption('closable', this.options.closable); | |
9f959ced | 9637 | |
25763f41 | 9638 | // move target element into content |
6e3d3bf1 | 9639 | var $content = this.element.detach(); |
25763f41 | 9640 | this._content.html($content); |
9f959ced | 9641 | |
25763f41 AE |
9642 | // create modal view |
9643 | if (this.options.modal) { | |
5a1b4042 AE |
9644 | this._overlay = $('#jsWcfDialogOverlay'); |
9645 | if (!this._overlay.length) { | |
af2ddf45 | 9646 | this._overlay = $('<div id="jsWcfDialogOverlay" class="dialogOverlay" />').css({ height: '100%', zIndex: 399 }).hide().appendTo(document.body); |
5a1b4042 AE |
9647 | } |
9648 | ||
af7da802 | 9649 | if (this.options.closable && this.options.closeViaModal) { |
25763f41 AE |
9650 | this._overlay.click($.proxy(this.close, this)); |
9651 | ||
9652 | $(document).keyup($.proxy(function(event) { | |
9653 | if (event.keyCode && event.keyCode === $.ui.keyCode.ESCAPE) { | |
9654 | this.close(); | |
9655 | event.preventDefault(); | |
9656 | } | |
9657 | }, this)); | |
9658 | } | |
9659 | } | |
4445e78f | 9660 | |
42d7d2cc | 9661 | WCF.DOMNodeInsertedHandler.execute(); |
25763f41 | 9662 | }, |
ef097134 | 9663 | |
f74219d0 MK |
9664 | /** |
9665 | * Sets the given option to the given value. | |
9666 | * See the jQuery UI widget documentation for more. | |
9667 | */ | |
9668 | _setOption: function(key, value) { | |
9669 | this.options[key] = value; | |
9670 | ||
9671 | if (key == 'hideTitle' || key == 'title') { | |
9672 | if (!this.options.hideTitle && this.options.title != '') { | |
9673 | this._title.html(this.options.title).show(); | |
9674 | } else { | |
9675 | this._title.html(''); | |
9676 | } | |
9677 | } else if (key == 'closable' || key == 'closeButtonLabel') { | |
9678 | if (this.options.closable) { | |
9679 | this._closeButton.attr('title', this.options.closeButtonLabel).show().find('span').html(this.options.closeButtonLabel); | |
49b2955d | 9680 | |
42d7d2cc | 9681 | WCF.DOMNodeInsertedHandler.execute(); |
f74219d0 MK |
9682 | } else { |
9683 | this._closeButton.hide(); | |
9684 | } | |
9685 | } | |
9686 | ||
9687 | if ((!this.options.hideTitle && this.options.title != '') || this.options.closable) { | |
9688 | this._titlebar.show(); | |
9689 | } else { | |
9690 | this._titlebar.hide(); | |
9691 | } | |
9692 | ||
9693 | return this; | |
9694 | }, | |
9695 | ||
25763f41 AE |
9696 | /** |
9697 | * Opens this dialog. | |
9698 | */ | |
9699 | open: function() { | |
bd16ec38 AE |
9700 | // ignore script tags |
9701 | if (this.element === false) { | |
9702 | return; | |
9703 | } | |
9704 | ||
25763f41 AE |
9705 | if (this.isOpen()) { |
9706 | return; | |
9707 | } | |
9f959ced | 9708 | |
1e7d6513 | 9709 | if (this._overlay !== null) { |
5a1b4042 AE |
9710 | WCF.activeDialogs++; |
9711 | ||
9712 | if (WCF.activeDialogs === 1) { | |
9713 | this._overlay.show(); | |
9714 | } | |
1e7d6513 | 9715 | } |
9f959ced | 9716 | |
25763f41 AE |
9717 | this.render(); |
9718 | this._isOpen = true; | |
9719 | }, | |
9f959ced | 9720 | |
25763f41 | 9721 | /** |
28410a97 | 9722 | * Returns true if dialog is visible. |
25763f41 AE |
9723 | * |
9724 | * @return boolean | |
9725 | */ | |
9726 | isOpen: function() { | |
9727 | return this._isOpen; | |
9728 | }, | |
ef097134 | 9729 | |
25763f41 AE |
9730 | /** |
9731 | * Closes this dialog. | |
c2b4d92d | 9732 | * |
8a369709 MS |
9733 | * This function can be manually called, even if the dialog is set as not |
9734 | * closable by the user. | |
9735 | * | |
c2b4d92d | 9736 | * @param object event |
25763f41 | 9737 | */ |
c2b4d92d | 9738 | close: function(event) { |
8a369709 | 9739 | if (!this.isOpen()) { |
25763f41 AE |
9740 | return; |
9741 | } | |
9f959ced | 9742 | |
af7da802 AE |
9743 | if (this.options.closeConfirmMessage) { |
9744 | WCF.System.Confirmation.show(this.options.closeConfirmMessage, $.proxy(function(action) { | |
9745 | if (action === 'confirm') { | |
9746 | this._close(); | |
9747 | } | |
9748 | }, this)); | |
9749 | } | |
9750 | else { | |
9751 | this._close(); | |
9752 | } | |
9753 | ||
9754 | if (event !== undefined) { | |
9755 | event.preventDefault(); | |
9756 | } | |
9757 | }, | |
9758 | ||
9759 | /** | |
9760 | * Handles dialog closing, should never be called directly. | |
9761 | * | |
9762 | * @see $.ui.wcfDialog.close() | |
9763 | */ | |
9764 | _close: function() { | |
25763f41 AE |
9765 | this._isOpen = false; |
9766 | this._container.wcfFadeOut(); | |
9f959ced | 9767 | |
25763f41 | 9768 | if (this._overlay !== null) { |
5a1b4042 AE |
9769 | WCF.activeDialogs--; |
9770 | ||
9771 | if (WCF.activeDialogs === 0) { | |
9772 | this._overlay.hide(); | |
9773 | } | |
25763f41 | 9774 | } |
34e158d9 AE |
9775 | |
9776 | if (this.options.onClose !== null) { | |
9777 | this.options.onClose(); | |
9778 | } | |
25763f41 | 9779 | }, |
ef097134 | 9780 | |
6e3d3bf1 AE |
9781 | /** |
9782 | * Renders dialog on resize if visible. | |
9783 | */ | |
9784 | _resize: function() { | |
9785 | if (this.isOpen()) { | |
9786 | this.render(); | |
9787 | } | |
9788 | }, | |
9789 | ||
25763f41 AE |
9790 | /** |
9791 | * Renders this dialog, should be called whenever content is updated. | |
9792 | */ | |
4da866ad | 9793 | render: function() { |
caa67386 AE |
9794 | // check if this if dialog was previously hidden and container is fixed |
9795 | // at 0px (mobile optimization), in this case scroll to top | |
9796 | if (!this._container.is(':visible') && this._container.css('top') === '0px') { | |
9797 | window.scrollTo(0, 0); | |
9798 | } | |
9799 | ||
942d8ea7 AE |
9800 | // force dialog and it's contents to be visible |
9801 | this._container.show(); | |
9802 | this._content.children().show(); | |
9f959ced | 9803 | |
942d8ea7 AE |
9804 | // remove fixed content dimensions for calculation |
9805 | this._content.css({ | |
9806 | height: 'auto', | |
9807 | width: 'auto' | |
abe2607e | 9808 | }); |
9f959ced | 9809 | |
942d8ea7 AE |
9810 | // terminate concurrent rendering processes |
9811 | this._container.stop(); | |
9812 | this._content.stop(); | |
9813 | ||
9814 | // set dialog to be fully opaque, prevents weird bugs in WebKit | |
9815 | this._container.show().css('opacity', 1.0); | |
9f959ced | 9816 | |
942d8ea7 AE |
9817 | // handle positioning of form submit controls |
9818 | var $heightDifference = 0; | |
a41af14a | 9819 | if (this._content.find('.formSubmit').length) { |
942d8ea7 AE |
9820 | $heightDifference = this._content.find('.formSubmit').outerHeight(); |
9821 | ||
9822 | this._content.addClass('dialogForm').css({ marginBottom: $heightDifference + 'px' }); | |
a41af14a MW |
9823 | } |
9824 | else { | |
9a9c5d2c | 9825 | this._content.removeClass('dialogForm').css({ marginBottom: '0px' }); |
a41af14a MW |
9826 | } |
9827 | ||
7200f12a | 9828 | // force 800px or 90% width |
25763f41 | 9829 | var $windowDimensions = $(window).getDimensions(); |
7200f12a AE |
9830 | if ($windowDimensions.width * 0.9 > 800) { |
9831 | this._container.css('maxWidth', '800px'); | |
845f9d84 AE |
9832 | } |
9833 | ||
9834 | // calculate dimensions | |
25763f41 | 9835 | var $containerDimensions = this._container.getDimensions('outer'); |
d7d9f722 | 9836 | var $contentDimensions = this._content.getDimensions(); |
9f959ced | 9837 | |
abe2607e AE |
9838 | // calculate maximum content height |
9839 | var $heightDifference = $containerDimensions.height - $contentDimensions.height; | |
49b2955d | 9840 | var $maximumHeight = $windowDimensions.height - $heightDifference - 120; |
abe2607e AE |
9841 | this._content.css({ maxHeight: $maximumHeight + 'px' }); |
9842 | ||
0ebf91e4 | 9843 | this._determineOverflow(); |
9f959ced | 9844 | |
9acd5123 | 9845 | // calculate new dimensions |
0ebf91e4 | 9846 | $containerDimensions = this._container.getDimensions('outer'); |
9f959ced | 9847 | |
25763f41 AE |
9848 | // move container |
9849 | var $leftOffset = Math.round(($windowDimensions.width - $containerDimensions.width) / 2); | |
9850 | var $topOffset = Math.round(($windowDimensions.height - $containerDimensions.height) / 2); | |
9f959ced | 9851 | |
25763f41 AE |
9852 | // place container at 20% height if possible |
9853 | var $desiredTopOffset = Math.round(($windowDimensions.height / 100) * 20); | |
9854 | if ($desiredTopOffset < $topOffset) { | |
9855 | $topOffset = $desiredTopOffset; | |
9856 | } | |
9f959ced | 9857 | |
942d8ea7 AE |
9858 | // apply offset |
9859 | this._container.css({ | |
9860 | left: $leftOffset + 'px', | |
9861 | top: $topOffset + 'px' | |
9862 | }); | |
9863 | ||
9864 | // remove static dimensions | |
9865 | this._content.css({ | |
9866 | height: 'auto', | |
942d8ea7 AE |
9867 | width: 'auto' |
9868 | }); | |
9869 | ||
9870 | if (!this.isOpen()) { | |
25763f41 AE |
9871 | // hide container again |
9872 | this._container.hide(); | |
9f959ced | 9873 | |
25763f41 | 9874 | // fade in container |
9acd5123 | 9875 | this._container.wcfFadeIn($.proxy(function() { |
942d8ea7 AE |
9876 | if (this.options.onShow !== null) { |
9877 | this.options.onShow(); | |
9878 | } | |
79537c05 | 9879 | }, this)); |
9acd5123 | 9880 | } |
9acd5123 | 9881 | }, |
9f959ced | 9882 | |
c1b31f6f AE |
9883 | /** |
9884 | * Determines content overflow based upon static dimensions. | |
9885 | */ | |
9886 | _determineOverflow: function() { | |
9887 | var $max = $(window).getDimensions(); | |
9888 | var $maxHeight = this._content.css('maxHeight'); | |
9889 | this._content.css('maxHeight', 'none'); | |
9890 | var $dialog = this._container.getDimensions('outer'); | |
9891 | ||
9892 | var $overflow = 'visible'; | |
9893 | if (($max.height * 0.8 < $dialog.height) || ($max.width * 0.8 < $dialog.width)) { | |
9894 | $overflow = 'auto'; | |
9895 | } | |
9896 | ||
9897 | this._content.css('overflow', $overflow); | |
9898 | this._content.css('maxHeight', $maxHeight); | |
c68d2056 AE |
9899 | |
9900 | if ($overflow === 'visible') { | |
9901 | // content may already overflow, even though the overall height is still below the threshold | |
9902 | var $contentHeight = 0; | |
9903 | this._content.children().each(function(index, child) { | |
9904 | $contentHeight += $(child).outerHeight(); | |
9905 | }); | |
9906 | ||
9907 | if (this._content.height() < $contentHeight) { | |
9908 | this._content.css('overflow', 'auto'); | |
9909 | } | |
9910 | } | |
c1b31f6f AE |
9911 | }, |
9912 | ||
9acd5123 AE |
9913 | /** |
9914 | * Returns calculated content dimensions. | |
9915 | * | |
9916 | * @param integer maximumHeight | |
9917 | * @return object | |
9918 | */ | |
9919 | _getContentDimensions: function(maximumHeight) { | |
9920 | var $contentDimensions = this._content.getDimensions(); | |
9f959ced | 9921 | |
9acd5123 | 9922 | // set height to maximum height if exceeded |
942d8ea7 | 9923 | if (maximumHeight && $contentDimensions.height > maximumHeight) { |
9acd5123 | 9924 | $contentDimensions.height = maximumHeight; |
25763f41 | 9925 | } |
9f959ced | 9926 | |
9acd5123 | 9927 | return $contentDimensions; |
25763f41 AE |
9928 | } |
9929 | }); | |
9930 | ||
848d0782 AE |
9931 | /** |
9932 | * Provides a slideshow for lists. | |
9933 | */ | |
9934 | $.widget('ui.wcfSlideshow', { | |
9935 | /** | |
9936 | * button list object | |
9937 | * @var jQuery | |
9938 | */ | |
9939 | _buttonList: null, | |
9940 | ||
9941 | /** | |
9942 | * number of items | |
9943 | * @var integer | |
9944 | */ | |
9945 | _count: 0, | |
9946 | ||
9947 | /** | |
9948 | * item index | |
9949 | * @var integer | |
9950 | */ | |
9951 | _index: 0, | |
9952 | ||
9953 | /** | |
9954 | * item list object | |
9955 | * @var jQuery | |
9956 | */ | |
9957 | _itemList: null, | |
9958 | ||
9959 | /** | |
9960 | * list of items | |
9961 | * @var jQuery | |
9962 | */ | |
9963 | _items: null, | |
9964 | ||
9965 | /** | |
9966 | * timer object | |
9967 | * @var WCF.PeriodicalExecuter | |
9968 | */ | |
9969 | _timer: null, | |
9970 | ||
9971 | /** | |
9972 | * list item width | |
9973 | * @var integer | |
9974 | */ | |
9975 | _width: 0, | |
9976 | ||
9977 | /** | |
9978 | * list of options | |
9979 | * @var object | |
9980 | */ | |
9981 | options: { | |
9982 | /* enables automatic cycling of items */ | |
9983 | cycle: true, | |
9984 | /* cycle interval in seconds */ | |
9985 | cycleInterval: 5, | |
9986 | /* gap between items in pixels */ | |
9987 | itemGap: 50, | |
9988 | }, | |
9989 | ||
9990 | /** | |
9991 | * Creates a new instance of ui.wcfSlideshow. | |
9992 | */ | |
9993 | _create: function() { | |
9994 | this._itemList = this.element.children('ul'); | |
9995 | this._items = this._itemList.children('li'); | |
9996 | this._count = this._items.length; | |
9997 | this._index = 0; | |
9998 | ||
b567c0c6 MW |
9999 | if (this._count > 1) { |
10000 | this._initSlideshow(); | |
10001 | } | |
848d0782 AE |
10002 | }, |
10003 | ||
10004 | /** | |
10005 | * Initializes the slideshow. | |
10006 | */ | |
10007 | _initSlideshow: function() { | |
10008 | // calculate item dimensions | |
10009 | var $itemHeight = $(this._items.get(0)).outerHeight(); | |
10010 | this._items.addClass('slideshowItem'); | |
10011 | this._width = this.element.css('height', $itemHeight).innerWidth(); | |
10012 | this._itemList.addClass('slideshowItemList').css('left', 0); | |
10013 | ||
10014 | this._items.each($.proxy(function(index, item) { | |
10015 | $(item).show().css({ | |
10016 | height: $itemHeight, | |
10017 | left: ((this._width + this.options.itemGap) * index), | |
10018 | width: this._width | |
10019 | }); | |
10020 | }, this)); | |
10021 | ||
10022 | this.element.css({ | |
10023 | height: $itemHeight, | |
10024 | width: this._width | |
10025 | }).hover($.proxy(this._hoverIn, this), $.proxy(this._hoverOut, this)); | |
10026 | ||
10027 | // create toggle buttons | |
b567c0c6 MW |
10028 | this._buttonList = $('<ul class="slideshowButtonList" />').appendTo(this.element); |
10029 | for (var $i = 0; $i < this._count; $i++) { | |
10030 | var $link = $('<li><a><span class="icon icon16 icon-circle" /></a></li>').data('index', $i).click($.proxy(this._click, this)).appendTo(this._buttonList); | |
10031 | if ($i == 0) { | |
10032 | $link.find('.icon').addClass('active'); | |
848d0782 AE |
10033 | } |
10034 | } | |
10035 | ||
b567c0c6 MW |
10036 | this._resetTimer(); |
10037 | ||
848d0782 AE |
10038 | $(window).resize($.proxy(this._resize, this)); |
10039 | }, | |
10040 | ||
10041 | /** | |
10042 | * Handles browser resizing | |
10043 | */ | |
10044 | _resize: function() { | |
10045 | this._width = this.element.css('width', 'auto').innerWidth(); | |
10046 | this._items.each($.proxy(function(index, item) { | |
10047 | $(item).css({ | |
10048 | left: ((this._width + this.options.itemGap) * index), | |
10049 | width: this._width | |
10050 | }); | |
10051 | }, this)); | |
10052 | ||
10053 | this._index--; | |
10054 | this.moveTo(null); | |
10055 | }, | |
10056 | ||
10057 | /** | |
10058 | * Disables cycling while hovering. | |
10059 | */ | |
10060 | _hoverIn: function() { | |
10061 | if (this._timer !== null) { | |
10062 | this._timer.stop(); | |
10063 | } | |
10064 | }, | |
10065 | ||
10066 | /** | |
10067 | * Enables cycling after mouse out. | |
10068 | */ | |
10069 | _hoverOut: function() { | |
10070 | this._resetTimer(); | |
10071 | }, | |
10072 | ||
10073 | /** | |
10074 | * Resets cycle timer. | |
10075 | */ | |
10076 | _resetTimer: function() { | |
10077 | if (!this.options.cycle) { | |
10078 | return; | |
10079 | } | |
10080 | ||
10081 | if (this._timer !== null) { | |
10082 | this._timer.stop(); | |
10083 | } | |
10084 | ||
10085 | var self = this; | |
10086 | this._timer = new WCF.PeriodicalExecuter(function() { | |
10087 | self.moveTo(null); | |
10088 | }, this.options.cycleInterval * 1000); | |
10089 | }, | |
10090 | ||
10091 | /** | |
10092 | * Handles clicks on the select buttons. | |
10093 | * | |
10094 | * @param object event | |
10095 | */ | |
10096 | _click: function(event) { | |
10097 | this.moveTo($(event.currentTarget).data('index')); | |
10098 | ||
10099 | this._resetTimer(); | |
10100 | }, | |
10101 | ||
10102 | /** | |
10103 | * Moves to a specified item index, NULL will move to the next item in list. | |
10104 | * | |
10105 | * @param integer index | |
10106 | */ | |
10107 | moveTo: function(index) { | |
10108 | this._index = (index === null) ? this._index + 1 : index; | |
10109 | if (this._index == this._count) { | |
10110 | this._index = 0; | |
10111 | } | |
10112 | ||
10113 | $(this._buttonList.find('.icon').removeClass('active').get(this._index)).addClass('active'); | |
10114 | this._itemList.css('left', this._index * (this._width + this.options.itemGap) * -1); | |
c96906ac AE |
10115 | |
10116 | this._trigger('moveTo', null, { index: this._index }); | |
10117 | }, | |
10118 | ||
10119 | /** | |
10120 | * Returns item by index or null if index is invalid. | |
10121 | * | |
10122 | * @return jQuery | |
10123 | */ | |
10124 | getItem: function(index) { | |
10125 | if (this._items[index]) { | |
10126 | return this._items[index]; | |
10127 | } | |
10128 | ||
10129 | return null; | |
848d0782 AE |
10130 | } |
10131 | }); | |
10132 | ||
158bd3ca | 10133 | /** |
18404c0a | 10134 | * Custom tab menu implementation for WCF. |
158bd3ca TD |
10135 | */ |
10136 | $.widget('ui.wcfTabs', $.ui.tabs, { | |
18404c0a AE |
10137 | /** |
10138 | * Workaround for ids containing a dot ".", until jQuery UI devs learn | |
10139 | * to properly escape ids ... (it took 18 months until they finally | |
10140 | * fixed it!) | |
10141 | * | |
10142 | * @see http://bugs.jqueryui.com/ticket/4681 | |
10143 | * @see $.ui.tabs.prototype._sanitizeSelector() | |
10144 | */ | |
158bd3ca TD |
10145 | _sanitizeSelector: function(hash) { |
10146 | return hash.replace(/([:\.])/g, '\\$1'); | |
18404c0a | 10147 | }, |
f24f0823 | 10148 | |
18404c0a AE |
10149 | /** |
10150 | * @see $.ui.tabs.prototype.select() | |
10151 | */ | |
10152 | select: function(index) { | |
10153 | if (!$.isNumeric(index)) { | |
10154 | // panel identifier given | |
10155 | this.panels.each(function(i, panel) { | |
10156 | if ($(panel).wcfIdentify() === index) { | |
10157 | index = i; | |
10158 | return false; | |
10159 | } | |
10160 | }); | |
10161 | ||
10162 | // unable to identify panel | |
10163 | if (!$.isNumeric(index)) { | |
10164 | console.debug("[ui.wcfTabs] Unable to find panel identified by '" + index + "', aborting."); | |
10165 | return; | |
10166 | } | |
10167 | } | |
dbd319de | 10168 | |
1682f3cb | 10169 | this._setOption('active', index); |
18404c0a | 10170 | }, |
9f959ced | 10171 | |
22b63e33 AE |
10172 | /** |
10173 | * Selects a specific tab by triggering the 'click' event. | |
10174 | * | |
10175 | * @param string tabIdentifier | |
10176 | */ | |
10177 | selectTab: function(tabIdentifier) { | |
10178 | tabIdentifier = '#' + tabIdentifier; | |
10179 | ||
10180 | this.anchors.each(function(index, anchor) { | |
10181 | var $anchor = $(anchor); | |
10182 | if ($anchor.prop('hash') === tabIdentifier) { | |
10183 | $anchor.trigger('click'); | |
10184 | return false; | |
10185 | } | |
10186 | }); | |
10187 | }, | |
10188 | ||
18404c0a AE |
10189 | /** |
10190 | * Returns the currently selected tab index. | |
10191 | * | |
10192 | * @return integer | |
10193 | */ | |
10194 | getCurrentIndex: function() { | |
f4126129 | 10195 | return this.lis.index(this.lis.filter('.ui-tabs-selected')); |
f24f0823 AE |
10196 | }, |
10197 | ||
10198 | /** | |
28410a97 | 10199 | * Returns true if identifier is used by an anchor. |
f24f0823 AE |
10200 | * |
10201 | * @param string identifier | |
10202 | * @param boolean isChildren | |
10203 | * @return boolean | |
10204 | */ | |
10205 | hasAnchor: function(identifier, isChildren) { | |
10206 | var $matches = false; | |
10207 | ||
10208 | this.anchors.each(function(index, anchor) { | |
10209 | var $href = $(anchor).attr('href'); | |
10210 | if (/#.+/.test($href)) { | |
10211 | // split by anchor | |
10212 | var $parts = $href.split('#', 2); | |
10213 | if (isChildren) { | |
10214 | $parts = $parts[1].split('-', 2); | |
10215 | } | |
10216 | ||
10217 | if ($parts[1] === identifier) { | |
10218 | $matches = true; | |
10219 | ||
10220 | // terminate loop | |
10221 | return false; | |
10222 | } | |
10223 | } | |
10224 | }); | |
10225 | ||
10226 | return $matches; | |
3d57a2dd AE |
10227 | }, |
10228 | ||
10229 | /** | |
10230 | * Shows default tab. | |
10231 | */ | |
10232 | revertToDefault: function() { | |
10233 | var $active = this.element.data('active'); | |
10234 | if (!$active || $active === '') $active = 0; | |
10235 | ||
10236 | this.select($active); | |
ef995fd9 AE |
10237 | }, |
10238 | ||
10239 | /** | |
10240 | * @see $.ui.tabs.prototype._processTabs() | |
10241 | */ | |
10242 | _processTabs: function() { | |
10243 | var that = this; | |
7b608580 | 10244 | |
ef995fd9 AE |
10245 | this.tablist = this._getList() |
10246 | .addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ) | |
10247 | .attr( "role", "tablist" ); | |
7b608580 | 10248 | |
ef995fd9 AE |
10249 | this.tabs = this.tablist.find( "> li:has(a[href])" ) |
10250 | .addClass( "ui-state-default ui-corner-top" ) | |
10251 | .attr({ | |
10252 | role: "tab", | |
10253 | tabIndex: -1 | |
10254 | }); | |
7b608580 | 10255 | |
ef995fd9 AE |
10256 | this.anchors = this.tabs.map(function() { |
10257 | return $( "a", this )[ 0 ]; | |
10258 | }) | |
10259 | .addClass( "ui-tabs-anchor" ) | |
10260 | .attr({ | |
10261 | role: "presentation", | |
10262 | tabIndex: -1 | |
10263 | }); | |
7b608580 | 10264 | |
ef995fd9 | 10265 | this.panels = $(); |
7b608580 | 10266 | |
ef995fd9 AE |
10267 | this.anchors.each(function( i, anchor ) { |
10268 | var selector, panel, | |
10269 | anchorId = $( anchor ).uniqueId().attr( "id" ), | |
10270 | tab = $( anchor ).closest( "li" ), | |
10271 | originalAriaControls = tab.attr( "aria-controls" ); | |
7b608580 | 10272 | |
ef995fd9 AE |
10273 | // inline tab |
10274 | selector = anchor.hash; | |
10275 | panel = that.element.find( that._sanitizeSelector( selector ) ); | |
10276 | ||
10277 | if ( panel.length) { | |
10278 | that.panels = that.panels.add( panel ); | |
10279 | } | |
10280 | if ( originalAriaControls ) { | |
10281 | tab.data( "ui-tabs-aria-controls", originalAriaControls ); | |
10282 | } | |
10283 | tab.attr({ | |
10284 | "aria-controls": selector.substring( 1 ), | |
10285 | "aria-labelledby": anchorId | |
10286 | }); | |
10287 | panel.attr( "aria-labelledby", anchorId ); | |
10288 | }); | |
7b608580 | 10289 | |
ef995fd9 AE |
10290 | this.panels |
10291 | .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) | |
10292 | .attr( "role", "tabpanel" ); | |
10293 | }, | |
10294 | ||
10295 | /** | |
10296 | * @see $.ui.tabs.prototype.load() | |
10297 | */ | |
10298 | load: function( index, event ) { | |
10299 | return; | |
158bd3ca TD |
10300 | } |
10301 | }); | |
10302 | ||
10303 | /** | |
10304 | * jQuery widget implementation of the wcf pagination. | |
10305 | */ | |
10306 | $.widget('ui.wcfPages', { | |
10307 | SHOW_LINKS: 11, | |
10308 | SHOW_SUB_LINKS: 20, | |
10309 | ||
10310 | options: { | |
10311 | // vars | |
10312 | activePage: 1, | |
10313 | maxPage: 1, | |
10314 | ||
158bd3ca TD |
10315 | // language |
10316 | // we use options here instead of language variables, because the paginator is not only usable with pages | |
10317 | nextPage: null, | |
9c1e5045 | 10318 | previousPage: null |
158bd3ca TD |
10319 | }, |
10320 | ||
10321 | /** | |
10322 | * Creates the pages widget. | |
10323 | */ | |
10324 | _create: function() { | |
10325 | if (this.options.nextPage === null) this.options.nextPage = WCF.Language.get('wcf.global.page.next'); | |
10326 | if (this.options.previousPage === null) this.options.previousPage = WCF.Language.get('wcf.global.page.previous'); | |
10327 | ||
b9698c4c | 10328 | this.element.addClass('pageNavigation'); |
158bd3ca TD |
10329 | |
10330 | this._render(); | |
10331 | }, | |
10332 | ||
10333 | /** | |
10334 | * Destroys the pages widget. | |
10335 | */ | |
10336 | destroy: function() { | |
10337 | $.Widget.prototype.destroy.apply(this, arguments); | |
10338 | ||
10339 | this.element.children().remove(); | |
10340 | }, | |
10341 | ||
10342 | /** | |
ebbf5629 | 10343 | * Renders the pages widget. |
158bd3ca TD |
10344 | */ |
10345 | _render: function() { | |
10346 | // only render if we have more than 1 page | |
10347 | if (!this.options.disabled && this.options.maxPage > 1) { | |
3bdc6920 AE |
10348 | var $hasHiddenPages = false; |
10349 | ||
158bd3ca TD |
10350 | // make sure pagination is visible |
10351 | if (this.element.hasClass('hidden')) { | |
10352 | this.element.removeClass('hidden'); | |
10353 | } | |
10354 | this.element.show(); | |
10355 | ||
10356 | this.element.children().remove(); | |
10357 | ||
556973c1 | 10358 | var $pageList = $('<ul />'); |
158bd3ca TD |
10359 | this.element.append($pageList); |
10360 | ||
556973c1 | 10361 | var $previousElement = $('<li class="button skip" />'); |
158bd3ca TD |
10362 | $pageList.append($previousElement); |
10363 | ||
10364 | if (this.options.activePage > 1) { | |
10365 | var $previousLink = $('<a' + ((this.options.previousPage != null) ? (' title="' + this.options.previousPage + '"') : ('')) + '></a>'); | |
10366 | $previousElement.append($previousLink); | |
10367 | this._bindSwitchPage($previousLink, this.options.activePage - 1); | |
10368 | ||
556973c1 | 10369 | var $previousImage = $('<span class="icon icon16 icon-double-angle-left" />'); |
158bd3ca TD |
10370 | $previousLink.append($previousImage); |
10371 | } | |
10372 | else { | |
556973c1 | 10373 | var $previousImage = $('<span class="icon icon16 icon-double-angle-left" />'); |
158bd3ca | 10374 | $previousElement.append($previousImage); |
d62d0d94 | 10375 | $previousElement.addClass('disabled').removeClass('button'); |
5814a7f5 | 10376 | $previousImage.addClass('disabled'); |
158bd3ca | 10377 | } |
158bd3ca TD |
10378 | |
10379 | // add first page | |
10380 | $pageList.append(this._renderLink(1)); | |
10381 | ||
10382 | // calculate page links | |
10383 | var $maxLinks = this.SHOW_LINKS - 4; | |
10384 | var $linksBefore = this.options.activePage - 2; | |
10385 | if ($linksBefore < 0) $linksBefore = 0; | |
10386 | var $linksAfter = this.options.maxPage - (this.options.activePage + 1); | |
10387 | if ($linksAfter < 0) $linksAfter = 0; | |
10388 | if (this.options.activePage > 1 && this.options.activePage < this.options.maxPage) $maxLinks--; | |
10389 | ||
10390 | var $half = $maxLinks / 2; | |
10391 | var $left = this.options.activePage; | |
10392 | var $right = this.options.activePage; | |
10393 | if ($left < 1) $left = 1; | |
10394 | if ($right < 1) $right = 1; | |
10395 | if ($right > this.options.maxPage - 1) $right = this.options.maxPage - 1; | |
10396 | ||
10397 | if ($linksBefore >= $half) { | |
10398 | $left -= $half; | |
10399 | } | |
10400 | else { | |
10401 | $left -= $linksBefore; | |
10402 | $right += $half - $linksBefore; | |
10403 | } | |
10404 | ||
10405 | if ($linksAfter >= $half) { | |
10406 | $right += $half; | |
10407 | } | |
10408 | else { | |
10409 | $right += $linksAfter; | |
10410 | $left -= $half - $linksAfter; | |
10411 | } | |
10412 | ||
10413 | $right = Math.ceil($right); | |
10414 | $left = Math.ceil($left); | |
10415 | if ($left < 1) $left = 1; | |
10416 | if ($right > this.options.maxPage) $right = this.options.maxPage; | |
10417 | ||
10418 | // left ... links | |
10419 | if ($left > 1) { | |
10420 | if ($left - 1 < 2) { | |
10421 | $pageList.append(this._renderLink(2)); | |
10422 | } | |
10423 | else { | |
ec591c4f | 10424 | $('<li class="button jumpTo"><a title="' + WCF.Language.get('wcf.global.page.jumpTo') + '" class="jsTooltip">...</a></li>').appendTo($pageList); |
3bdc6920 | 10425 | $hasHiddenPages = true; |
158bd3ca TD |
10426 | } |
10427 | } | |
10428 | ||
10429 | // visible links | |
10430 | for (var $i = $left + 1; $i < $right; $i++) { | |
10431 | $pageList.append(this._renderLink($i)); | |
10432 | } | |
10433 | ||
10434 | // right ... links | |
10435 | if ($right < this.options.maxPage) { | |
10436 | if (this.options.maxPage - $right < 2) { | |
10437 | $pageList.append(this._renderLink(this.options.maxPage - 1)); | |
10438 | } | |
10439 | else { | |
ec591c4f | 10440 | $('<li class="button jumpTo"><a title="' + WCF.Language.get('wcf.global.page.jumpTo') + '" class="jsTooltip">...</a></li>').appendTo($pageList); |
3bdc6920 | 10441 | $hasHiddenPages = true; |
158bd3ca TD |
10442 | } |
10443 | } | |
10444 | ||
10445 | // add last page | |
10446 | $pageList.append(this._renderLink(this.options.maxPage)); | |
10447 | ||
10448 | // add next button | |
556973c1 | 10449 | var $nextElement = $('<li class="button skip" />'); |
158bd3ca TD |
10450 | $pageList.append($nextElement); |
10451 | ||
10452 | if (this.options.activePage < this.options.maxPage) { | |
b9698c4c | 10453 | var $nextLink = $('<a' + ((this.options.nextPage != null) ? (' title="' + this.options.nextPage + '"') : ('')) + '></a>'); |
158bd3ca TD |
10454 | $nextElement.append($nextLink); |
10455 | this._bindSwitchPage($nextLink, this.options.activePage + 1); | |
10456 | ||
556973c1 | 10457 | var $nextImage = $('<span class="icon icon16 icon-double-angle-right" />'); |
158bd3ca TD |
10458 | $nextLink.append($nextImage); |
10459 | } | |
10460 | else { | |
556973c1 | 10461 | var $nextImage = $('<span class="icon icon16 icon-double-angle-right" />'); |
158bd3ca | 10462 | $nextElement.append($nextImage); |
d62d0d94 | 10463 | $nextElement.addClass('disabled').removeClass('button'); |
5814a7f5 | 10464 | $nextImage.addClass('disabled'); |
158bd3ca | 10465 | } |
3bdc6920 AE |
10466 | |
10467 | if ($hasHiddenPages) { | |
a19475d3 | 10468 | $pageList.data('pages', this.options.maxPage); |
3bdc6920 AE |
10469 | WCF.System.PageNavigation.init('#' + $pageList.wcfIdentify(), $.proxy(function(pageNo) { |
10470 | this.switchPage(pageNo); | |
10471 | }, this)); | |
3bdc6920 | 10472 | } |
158bd3ca TD |
10473 | } |
10474 | else { | |
10475 | // otherwise hide the paginator if not already hidden | |
10476 | this.element.hide(); | |
10477 | } | |
10478 | }, | |
10479 | ||
10480 | /** | |
9f959ced | 10481 | * Renders a page link. |
158bd3ca TD |
10482 | * |
10483 | * @parameter integer page | |
9f959ced | 10484 | * @return jQuery |
158bd3ca TD |
10485 | */ |
10486 | _renderLink: function(page, lineBreak) { | |
b9698c4c | 10487 | var $pageElement = $('<li class="button"></li>'); |
158bd3ca TD |
10488 | if (lineBreak != undefined && lineBreak) { |
10489 | $pageElement.addClass('break'); | |
10490 | } | |
10491 | if (page != this.options.activePage) { | |
10492 | var $pageLink = $('<a>' + WCF.String.addThousandsSeparator(page) + '</a>'); | |
10493 | $pageElement.append($pageLink); | |
10494 | this._bindSwitchPage($pageLink, page); | |
10495 | } | |
10496 | else { | |
10497 | $pageElement.addClass('active'); | |
10498 | var $pageSubElement = $('<span>' + WCF.String.addThousandsSeparator(page) + '</span>'); | |
10499 | $pageElement.append($pageSubElement); | |
10500 | } | |
10501 | ||
10502 | return $pageElement; | |
10503 | }, | |
10504 | ||
10505 | /** | |
10506 | * Binds the 'click'-event for the page switching to the given element. | |
10507 | * | |
10508 | * @parameter $(element) element | |
10509 | * @paremeter integer page | |
10510 | */ | |
10511 | _bindSwitchPage: function(element, page) { | |
10512 | var $self = this; | |
10513 | element.click(function() { | |
10514 | $self.switchPage(page); | |
10515 | }); | |
10516 | }, | |
10517 | ||
10518 | /** | |
10519 | * Switches to the given page | |
10520 | * | |
10521 | * @parameter Event event | |
10522 | * @parameter integer page | |
10523 | */ | |
10524 | switchPage: function(page) { | |
10525 | this._setOption('activePage', page); | |
10526 | }, | |
10527 | ||
10528 | /** | |
10529 | * Sets the given option to the given value. | |
10530 | * See the jQuery UI widget documentation for more. | |
10531 | */ | |
10532 | _setOption: function(key, value) { | |
10533 | if (key == 'activePage') { | |
10534 | if (value != this.options[key] && value > 0 && value <= this.options.maxPage) { | |
10535 | // you can prevent the page switching by returning false or by event.preventDefault() | |
10536 | // in a shouldSwitch-callback. e.g. if an AJAX request is already running. | |
10537 | var $result = this._trigger('shouldSwitch', undefined, { | |
81d1b3b5 | 10538 | nextPage: value |
158bd3ca TD |
10539 | }); |
10540 | ||
a19475d3 | 10541 | if ($result || $result !== undefined) { |
158bd3ca TD |
10542 | this.options[key] = value; |
10543 | this._render(); | |
10544 | this._trigger('switched', undefined, { | |
81d1b3b5 | 10545 | activePage: value |
158bd3ca TD |
10546 | }); |
10547 | } | |
10548 | else { | |
10549 | this._trigger('notSwitched', undefined, { | |
81d1b3b5 | 10550 | activePage: value |
158bd3ca TD |
10551 | }); |
10552 | } | |
10553 | } | |
10554 | } | |
10555 | else { | |
10556 | this.options[key] = value; | |
10557 | ||
10558 | if (key == 'disabled') { | |
10559 | if (value) { | |
10560 | this.element.children().remove(); | |
10561 | } | |
10562 | else { | |
f4126129 | 10563 | this._render(); |
158bd3ca TD |
10564 | } |
10565 | } | |
10566 | else if (key == 'maxPage') { | |
10567 | this._render(); | |
10568 | } | |
10569 | } | |
10570 | ||
10571 | return this; | |
10572 | }, | |
10573 | ||
10574 | /** | |
10575 | * Start input of pagenumber | |
10576 | * | |
10577 | * @parameter Event event | |
10578 | */ | |
10579 | _startInput: function(event) { | |
10580 | // hide a-tag | |
10581 | var $childLink = $(event.currentTarget); | |
10582 | if (!$childLink.is('a')) $childLink = $childLink.parent('a'); | |
10583 | ||
10584 | $childLink.hide(); | |
10585 | ||
10586 | // show input-tag | |
10587 | var $childInput = $childLink.parent('li').children('input') | |
10588 | .css('display', 'block') | |
10589 | .val(''); | |
10590 | ||
10591 | $childInput.focus(); | |
10592 | }, | |
10593 | ||
10594 | /** | |
10595 | * Stops input of pagenumber | |
10596 | * | |
10597 | * @parameter Event event | |
10598 | */ | |
10599 | _stopInput: function(event) { | |
10600 | // hide input-tag | |
10601 | var $childInput = $(event.currentTarget); | |
10602 | $childInput.css('display', 'none'); | |
10603 | ||
10604 | // show a-tag | |
f4126129 | 10605 | var $childContainer = $childInput.parent('li'); |
158bd3ca TD |
10606 | if ($childContainer != undefined && $childContainer != null) { |
10607 | $childContainer.children('a').show(); | |
10608 | } | |
10609 | }, | |
10610 | ||
10611 | /** | |
10612 | * Handles input of pagenumber | |
10613 | * | |
10614 | * @parameter Event event | |
10615 | */ | |
10616 | _handleInput: function(event) { | |
10617 | var $ie7 = ($.browser.msie && $.browser.version == '7.0'); | |
10618 | if (event.type != 'keyup' || $ie7) { | |
10619 | if (!$ie7 || ((event.which == 13 || event.which == 27) && event.type == 'keyup')) { | |
10620 | if (event.which == 13) { | |
10621 | this.switchPage(parseInt($(event.currentTarget).val())); | |
10622 | } | |
10623 | ||
10624 | if (event.which == 13 || event.which == 27) { | |
10625 | this._stopInput(event); | |
10626 | event.stopPropagation(); | |
10627 | } | |
10628 | } | |
10629 | } | |
10630 | } | |
10631 | }); | |
10632 | ||
02e410dc MW |
10633 | /** |
10634 | * Namespace for category related classes. | |
10635 | */ | |
10636 | WCF.Category = { }; | |
10637 | ||
10638 | /** | |
10639 | * Handles selection of categories. | |
10640 | */ | |
10641 | WCF.Category.NestedList = Class.extend({ | |
10642 | /** | |
10643 | * list of categories | |
10644 | * @var object | |
10645 | */ | |
10646 | _categories: { }, | |
10647 | ||
10648 | /** | |
10649 | * Initializes the WCF.Category.NestedList object. | |
10650 | */ | |
10651 | init: function() { | |
10652 | var self = this; | |
10653 | $('.jsCategory').each(function(index, category) { | |
10654 | var $category = $(category).data('parentCategoryID', null).change($.proxy(self._updateSelection, self)); | |
10655 | self._categories[$category.val()] = $category; | |
10656 | ||
10657 | // find child categories | |
10658 | var $childCategoryIDs = [ ]; | |
10659 | $category.parents('li').find('.jsChildCategory').each(function(innerIndex, childCategory) { | |
10660 | var $childCategory = $(childCategory).data('parentCategoryID', $category.val()).change($.proxy(self._updateSelection, self)); | |
10661 | self._categories[$childCategory.val()] = $childCategory; | |
10662 | $childCategoryIDs.push($childCategory.val()); | |
10663 | ||
10664 | if ($childCategory.is(':checked')) { | |
10665 | $category.prop('checked', 'checked'); | |
10666 | } | |
10667 | }); | |
10668 | ||
10669 | $category.data('childCategoryIDs', $childCategoryIDs); | |
10670 | }); | |
10671 | }, | |
10672 | ||
10673 | /** | |
10674 | * Updates selection of categories. | |
10675 | * | |
10676 | * @param object event | |
10677 | */ | |
10678 | _updateSelection: function(event) { | |
10679 | var $category = $(event.currentTarget); | |
10680 | var $parentCategoryID = $category.data('parentCategoryID'); | |
10681 | ||
10682 | if ($category.is(':checked')) { | |
10683 | // child category | |
10684 | if ($parentCategoryID !== null) { | |
10685 | // mark parent category as checked | |
10686 | this._categories[$parentCategoryID].prop('checked', 'checked'); | |
10687 | } | |
10688 | } | |
10689 | else { | |
10690 | // top-level category | |
10691 | if ($parentCategoryID === null) { | |
10692 | // unmark all child categories | |
10693 | var $childCategoryIDs = $category.data('childCategoryIDs'); | |
10694 | for (var $i = 0, $length = $childCategoryIDs.length; $i < $length; $i++) { | |
10695 | this._categories[$childCategoryIDs[$i]].prop('checked', false); | |
10696 | } | |
10697 | } | |
10698 | } | |
10699 | } | |
10700 | }); | |
10701 | ||
158bd3ca TD |
10702 | /** |
10703 | * Encapsulate eval() within an own function to prevent problems | |
10704 | * with optimizing and minifiny JS. | |
10705 | * | |
10706 | * @param mixed expression | |
10707 | * @returns mixed | |
10708 | */ | |
10709 | function wcfEval(expression) { | |
10710 | return eval(expression); | |
10711 | } |