Commit | Line | Data |
---|---|---|
158bd3ca TD |
1 | /** |
2 | * Class and function collection for WCF | |
3 | * | |
da27d58a | 4 | * @author Markus Bartz, Tim Düsterhus, Alexander Ebert, Matthias Schmidt |
158bd3ca TD |
5 | * @copyright 2001-2011 WoltLab GmbH |
6 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | |
7 | */ | |
8 | ||
e93829d0 AE |
9 | (function() { |
10 | // store original implementation | |
11 | var $jQueryData = jQuery.fn.data; | |
9f959ced | 12 | |
e93829d0 AE |
13 | /** |
14 | * Override jQuery.fn.data() to support custom 'ID' suffix which will | |
15 | * be translated to '-id' at runtime. | |
16 | * | |
17 | * @see jQuery.fn.data() | |
18 | */ | |
19 | jQuery.fn.data = function(key, value) { | |
1c93e6e7 AE |
20 | if (key && key.match(/ID$/)) { |
21 | arguments[0] = key.replace(/ID$/, '-id'); | |
e93829d0 | 22 | } |
9f959ced | 23 | |
e93829d0 | 24 | // call jQuery's own data method |
1c93e6e7 AE |
25 | var $data = $jQueryData.apply(this, arguments); |
26 | ||
27 | // handle .data() call without arguments | |
28 | if (key === undefined) { | |
29 | for (var $key in $data) { | |
30 | if ($key.match(/Id$/)) { | |
696930b9 | 31 | $data[$key.replace(/Id$/, 'ID')] = $data[$key]; |
1c93e6e7 AE |
32 | delete $data[$key]; |
33 | } | |
34 | } | |
35 | } | |
36 | ||
37 | return $data; | |
f4126129 | 38 | }; |
e93829d0 AE |
39 | })(); |
40 | ||
9f959ced MS |
41 | /** |
42 | * Simple JavaScript Inheritance | |
878d0d80 AE |
43 | * By John Resig http://ejohn.org/ |
44 | * MIT Licensed. | |
45 | */ | |
46 | // Inspired by base2 and Prototype | |
f4126129 | 47 | (function(){var a=false,b=/xyz/.test(function(){xyz})?/\b_super\b/:/.*/;this.Class=function(){};Class.extend=function(c){function g(){if(!a&&this.init)this.init.apply(this,arguments);}var d=this.prototype;a=true;var e=new this;a=false;for(var f in c){e[f]=typeof c[f]=="function"&&typeof d[f]=="function"&&b.test(c[f])?function(a,b){return function(){var c=this._super;this._super=d[a];var e=b.apply(this,arguments);this._super=c;return e;};}(f,c[f]):c[f]}g.prototype=e;g.prototype.constructor=g;g.extend=arguments.callee;return g;};})(); |
878d0d80 | 48 | |
0937237b AE |
49 | /** |
50 | * Provides a hashCode() method for strings, similar to Java's String.hashCode(). | |
51 | * | |
52 | * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ | |
53 | */ | |
54 | String.prototype.hashCode = function() { | |
55 | var $char; | |
56 | var $hash = 0; | |
57 | ||
58 | if (this.length) { | |
59 | for (var $i = 0, $length = this.length; $i < $length; $i++) { | |
60 | $char = this.charCodeAt($i); | |
61 | $hash = (($hash << 5) - $hash) + $char; | |
62 | $hash = $hash & $hash; // convert to 32bit integer | |
63 | } | |
64 | } | |
65 | ||
66 | return $hash; | |
7382b20f | 67 | }; |
0937237b | 68 | |
158bd3ca TD |
69 | /** |
70 | * Initialize WCF namespace | |
71 | */ | |
72 | var WCF = {}; | |
73 | ||
74 | /** | |
75 | * Extends jQuery with additional methods. | |
76 | */ | |
77 | $.extend(true, { | |
a009a689 | 78 | /** |
acf25f0f | 79 | * Removes the given value from the given array and returns the array. |
a009a689 MS |
80 | * |
81 | * @param array array | |
82 | * @param mixed element | |
83 | * @return array | |
84 | */ | |
85 | removeArrayValue: function(array, value) { | |
86 | return $.grep(array, function(element, index) { | |
87 | return value !== element; | |
88 | }); | |
89 | }, | |
90 | ||
158bd3ca TD |
91 | /** |
92 | * Escapes an ID to work with jQuery selectors. | |
93 | * | |
94 | * @see http://docs.jquery.com/Frequently_Asked_Questions#How_do_I_select_an_element_by_an_ID_that_has_characters_used_in_CSS_notation.3F | |
95 | * @param string id | |
96 | * @return string | |
97 | */ | |
98 | wcfEscapeID: function(id) { | |
99 | return id.replace(/(:|\.)/g, '\\$1'); | |
100 | }, | |
101 | ||
102 | /** | |
103 | * Returns true if given ID exists within DOM. | |
104 | * | |
105 | * @param string id | |
106 | * @return boolean | |
107 | */ | |
108 | wcfIsset: function(id) { | |
109 | return !!$('#' + $.wcfEscapeID(id)).length; | |
2b4d2743 | 110 | }, |
9f959ced | 111 | |
2b4d2743 AE |
112 | /** |
113 | * Returns the length of an object. | |
114 | * | |
115 | * @param object targetObject | |
116 | * @return integer | |
117 | */ | |
118 | getLength: function(targetObject) { | |
119 | var $length = 0; | |
9f959ced | 120 | |
2b4d2743 AE |
121 | for (var $key in targetObject) { |
122 | if (targetObject.hasOwnProperty($key)) { | |
123 | $length++; | |
124 | } | |
125 | } | |
126 | ||
127 | return $length; | |
158bd3ca TD |
128 | } |
129 | }); | |
130 | ||
131 | /** | |
132 | * Extends jQuery's chainable methods. | |
133 | */ | |
134 | $.fn.extend({ | |
135 | /** | |
136 | * Returns tag name of current jQuery element. | |
137 | * | |
138 | * @returns string | |
139 | */ | |
140 | getTagName: function() { | |
141 | return this.get(0).tagName.toLowerCase(); | |
142 | }, | |
143 | ||
144 | /** | |
145 | * Returns the dimensions for current element. | |
146 | * | |
147 | * @see http://api.jquery.com/hidden-selector/ | |
148 | * @param string type | |
149 | * @return object | |
150 | */ | |
151 | getDimensions: function(type) { | |
152 | var dimensions = css = {}; | |
153 | var wasHidden = false; | |
154 | ||
155 | // show element to retrieve dimensions and restore them later | |
156 | if (this.is(':hidden')) { | |
157 | css = { | |
158 | display: this.css('display'), | |
159 | visibility: this.css('visibility') | |
160 | }; | |
161 | ||
162 | wasHidden = true; | |
163 | ||
164 | this.css({ | |
165 | display: 'block', | |
166 | visibility: 'hidden' | |
167 | }); | |
168 | } | |
169 | ||
170 | switch (type) { | |
171 | case 'inner': | |
172 | dimensions = { | |
173 | height: this.innerHeight(), | |
174 | width: this.innerWidth() | |
175 | }; | |
176 | break; | |
177 | ||
178 | case 'outer': | |
179 | dimensions = { | |
180 | height: this.outerHeight(), | |
181 | width: this.outerWidth() | |
182 | }; | |
183 | break; | |
184 | ||
185 | default: | |
186 | dimensions = { | |
187 | height: this.height(), | |
188 | width: this.width() | |
189 | }; | |
190 | break; | |
191 | } | |
192 | ||
193 | // restore previous settings | |
194 | if (wasHidden) { | |
195 | this.css(css); | |
196 | } | |
197 | ||
198 | return dimensions; | |
199 | }, | |
200 | ||
201 | /** | |
202 | * Returns the offsets for current element, defaults to position | |
203 | * relative to document. | |
204 | * | |
205 | * @see http://api.jquery.com/hidden-selector/ | |
206 | * @param string type | |
207 | * @return object | |
208 | */ | |
209 | getOffsets: function(type) { | |
210 | var offsets = css = {}; | |
211 | var wasHidden = false; | |
212 | ||
213 | // show element to retrieve dimensions and restore them later | |
214 | if (this.is(':hidden')) { | |
215 | css = { | |
216 | display: this.css('display'), | |
217 | visibility: this.css('visibility') | |
218 | }; | |
219 | ||
220 | wasHidden = true; | |
221 | ||
222 | this.css({ | |
223 | display: 'block', | |
224 | visibility: 'hidden' | |
225 | }); | |
226 | } | |
227 | ||
228 | switch (type) { | |
229 | case 'offset': | |
230 | offsets = this.offset(); | |
231 | break; | |
232 | ||
233 | case 'position': | |
234 | default: | |
235 | offsets = this.position(); | |
236 | break; | |
237 | } | |
238 | ||
239 | // restore previous settings | |
240 | if (wasHidden) { | |
241 | this.css(css); | |
242 | } | |
243 | ||
244 | return offsets; | |
245 | }, | |
246 | ||
247 | /** | |
248 | * Changes element's position to 'absolute' or 'fixed' while maintaining it's | |
249 | * current position relative to viewport. Optionally removes element from | |
250 | * current DOM-node and moving it into body-element (useful for drag & drop) | |
251 | * | |
252 | * @param boolean rebase | |
253 | * @return object | |
254 | */ | |
255 | makePositioned: function(position, rebase) { | |
256 | if (position != 'absolute' && position != 'fixed') { | |
257 | position = 'absolute'; | |
258 | } | |
259 | ||
260 | var $currentPosition = this.getOffsets('position'); | |
261 | this.css({ | |
262 | position: position, | |
263 | left: $currentPosition.left, | |
264 | margin: 0, | |
265 | top: $currentPosition.top | |
266 | }); | |
267 | ||
268 | if (rebase) { | |
269 | this.remove().appentTo('body'); | |
270 | } | |
271 | ||
272 | return this; | |
273 | }, | |
274 | ||
275 | /** | |
276 | * Disables a form element. | |
277 | * | |
278 | * @return jQuery | |
279 | */ | |
280 | disable: function() { | |
281 | return this.attr('disabled', 'disabled'); | |
282 | }, | |
283 | ||
284 | /** | |
285 | * Enables a form element. | |
286 | * | |
287 | * @return jQuery | |
288 | */ | |
289 | enable: function() { | |
290 | return this.removeAttr('disabled'); | |
291 | }, | |
9f959ced | 292 | |
b3991cb3 AE |
293 | /** |
294 | * Returns the element's id. If none is set, a random unique | |
295 | * ID will be assigned. | |
296 | * | |
297 | * @return string | |
298 | */ | |
299 | wcfIdentify: function() { | |
300 | if (!this.attr('id')) { | |
301 | this.attr('id', WCF.getRandomID()); | |
302 | } | |
9f959ced | 303 | |
b3991cb3 AE |
304 | return this.attr('id'); |
305 | }, | |
158bd3ca | 306 | |
0bfde690 AE |
307 | /** |
308 | * Returns the caret position of current element. If the element | |
309 | * does not equal input[type=text], input[type=password] or | |
310 | * textarea, -1 is returned. | |
311 | * | |
312 | * @return integer | |
313 | */ | |
314 | getCaret: function() { | |
315 | if (this.getTagName() == 'input') { | |
316 | if (this.attr('type') != 'text' && this.attr('type') != 'password') { | |
317 | return -1; | |
318 | } | |
319 | } | |
320 | else if (this.getTagName() != 'textarea') { | |
321 | return -1; | |
322 | } | |
323 | ||
324 | var $position = 0; | |
325 | var $element = this.get(0); | |
326 | if (document.selection) { // IE 8 | |
327 | // set focus to enable caret on this element | |
328 | this.focus(); | |
329 | ||
330 | var $selection = document.selection.createRange(); | |
331 | $selection.moveStart('character', -this.val().length); | |
332 | $position = $selection.text.length; | |
333 | } | |
334 | else if ($element.selectionStart || $element.selectionStart == '0') { // Opera, Chrome, Firefox, Safari, IE 9+ | |
335 | $position = parseInt($element.selectionStart); | |
336 | } | |
337 | ||
338 | return $position; | |
339 | }, | |
340 | ||
f73ff47d TD |
341 | /** |
342 | * Sets the caret position of current element. If the element | |
343 | * does not equal input[type=text], input[type=password] or | |
344 | * textarea, false is returned. | |
345 | * | |
346 | * @param integer position | |
347 | * @return boolean | |
348 | */ | |
349 | setCaret: function (position) { | |
350 | if (this.getTagName() == 'input') { | |
351 | if (this.attr('type') != 'text' && this.attr('type') != 'password') { | |
352 | return false; | |
353 | } | |
354 | } | |
355 | else if (this.getTagName() != 'textarea') { | |
356 | return false; | |
357 | } | |
358 | ||
359 | var $element = this.get(0); | |
360 | ||
361 | // set focus to enable caret on this element | |
362 | this.focus(); | |
363 | if (document.selection) { // IE 8 | |
364 | var $selection = document.selection.createRange(); | |
365 | $selection.moveStart('character', position); | |
366 | $selection.moveEnd('character', 0); | |
367 | $selection.select(); | |
368 | } | |
369 | else if ($element.selectionStart || $element.selectionStart == '0') { // Opera, Chrome, Firefox, Safari, IE 9+ | |
370 | $element.selectionStart = position; | |
371 | $element.selectionEnd = position; | |
372 | } | |
373 | ||
374 | return true; | |
375 | }, | |
376 | ||
158bd3ca TD |
377 | /** |
378 | * Shows an element by sliding and fading it into viewport. | |
379 | * | |
380 | * @param string direction | |
381 | * @param object callback | |
453aced6 | 382 | * @param integer duration |
158bd3ca TD |
383 | * @returns jQuery |
384 | */ | |
453aced6 | 385 | wcfDropIn: function(direction, callback, duration) { |
158bd3ca | 386 | if (!direction) direction = 'up'; |
453aced6 | 387 | if (!duration || !parseInt(duration)) duration = 200; |
158bd3ca | 388 | |
453aced6 | 389 | return this.show(WCF.getEffect(this.getTagName(), 'drop'), { direction: direction }, duration, callback); |
158bd3ca TD |
390 | }, |
391 | ||
392 | /** | |
393 | * Hides an element by sliding and fading it out the viewport. | |
394 | * | |
395 | * @param string direction | |
396 | * @param object callback | |
453aced6 | 397 | * @param integer duration |
158bd3ca TD |
398 | * @returns jQuery |
399 | */ | |
453aced6 | 400 | wcfDropOut: function(direction, callback, duration) { |
158bd3ca | 401 | if (!direction) direction = 'down'; |
453aced6 | 402 | if (!duration || !parseInt(duration)) duration = 200; |
158bd3ca | 403 | |
453aced6 | 404 | return this.hide(WCF.getEffect(this.getTagName(), 'drop'), { direction: direction }, duration, callback); |
158bd3ca TD |
405 | }, |
406 | ||
407 | /** | |
408 | * Shows an element by blinding it up. | |
409 | * | |
410 | * @param string direction | |
411 | * @param object callback | |
78e4d558 | 412 | * @param integer duration |
158bd3ca TD |
413 | * @returns jQuery |
414 | */ | |
78e4d558 | 415 | wcfBlindIn: function(direction, callback, duration) { |
158bd3ca | 416 | if (!direction) direction = 'vertical'; |
78e4d558 | 417 | if (!duration || !parseInt(duration)) duration = 200; |
158bd3ca | 418 | |
78e4d558 | 419 | return this.show(WCF.getEffect(this.getTagName(), 'blind'), { direction: direction }, duration, callback); |
158bd3ca TD |
420 | }, |
421 | ||
422 | /** | |
423 | * Hides an element by blinding it down. | |
424 | * | |
425 | * @param string direction | |
426 | * @param object callback | |
78e4d558 | 427 | * @param integer duration |
158bd3ca TD |
428 | * @returns jQuery |
429 | */ | |
78e4d558 | 430 | wcfBlindOut: function(direction, callback, duration) { |
158bd3ca | 431 | if (!direction) direction = 'vertical'; |
78e4d558 | 432 | if (!duration || !parseInt(duration)) duration = 200; |
158bd3ca | 433 | |
78e4d558 | 434 | return this.hide(WCF.getEffect(this.getTagName(), 'blind'), { direction: direction }, duration, callback); |
158bd3ca TD |
435 | }, |
436 | ||
437 | /** | |
438 | * Highlights an element. | |
439 | * | |
440 | * @param object options | |
441 | * @param object callback | |
442 | * @returns jQuery | |
443 | */ | |
444 | wcfHighlight: function(options, callback) { | |
445 | return this.effect('highlight', options, 600, callback); | |
25763f41 | 446 | }, |
9f959ced | 447 | |
25763f41 AE |
448 | /** |
449 | * Shows an element by fading it in. | |
450 | * | |
451 | * @param object callback | |
452 | * @param integer duration | |
453 | * @returns jQuery | |
454 | */ | |
455 | wcfFadeIn: function(callback, duration) { | |
456 | if (!duration || !parseInt(duration)) duration = 200; | |
457 | ||
458 | return this.show(WCF.getEffect(this.getTagName(), 'fade'), { }, duration, callback); | |
459 | }, | |
9f959ced | 460 | |
25763f41 AE |
461 | /** |
462 | * Hides an element by fading it out. | |
463 | * | |
464 | * @param object callback | |
465 | * @param integer duration | |
466 | * @returns jQuery | |
467 | */ | |
468 | wcfFadeOut: function(callback, duration) { | |
469 | if (!duration || !parseInt(duration)) duration = 200; | |
470 | ||
471 | return this.hide(WCF.getEffect(this.getTagName(), 'fade'), { }, duration, callback); | |
158bd3ca TD |
472 | } |
473 | }); | |
474 | ||
475 | /** | |
476 | * WoltLab Community Framework core methods | |
477 | */ | |
478 | $.extend(WCF, { | |
5a1b4042 AE |
479 | /** |
480 | * count of active dialogs | |
481 | * @var integer | |
482 | */ | |
483 | activeDialogs: 0, | |
484 | ||
158bd3ca TD |
485 | /** |
486 | * Counter for dynamic element id's | |
487 | * | |
488 | * @var integer | |
489 | */ | |
490 | _idCounter: 0, | |
9f959ced | 491 | |
158bd3ca TD |
492 | /** |
493 | * Shows a modal dialog with a built-in AJAX-loader. | |
494 | * | |
495 | * @param string dialogID | |
496 | * @param boolean resetDialog | |
e5b88a7f | 497 | * @return jQuery |
158bd3ca TD |
498 | */ |
499 | showAJAXDialog: function(dialogID, resetDialog) { | |
500 | if (!dialogID) { | |
501 | dialogID = this.getRandomID(); | |
502 | } | |
503 | ||
504 | if (!$.wcfIsset(dialogID)) { | |
e5b88a7f | 505 | $('<div id="' + dialogID + '"></div>').appendTo(document.body); |
158bd3ca TD |
506 | } |
507 | ||
508 | var dialog = $('#' + $.wcfEscapeID(dialogID)); | |
509 | ||
510 | if (resetDialog) { | |
511 | dialog.empty(); | |
512 | } | |
513 | ||
158bd3ca | 514 | var dialogOptions = arguments[2] || {}; |
c556ba48 TD |
515 | dialogOptions.ajax = true; |
516 | ||
e5b88a7f | 517 | dialog.wcfDialog(dialogOptions); |
c556ba48 | 518 | |
e5b88a7f | 519 | return dialog; |
158bd3ca TD |
520 | }, |
521 | ||
522 | /** | |
523 | * Shows a modal dialog. | |
fb08f20f | 524 | * |
158bd3ca TD |
525 | * @param string dialogID |
526 | */ | |
36710781 | 527 | showDialog: function(dialogID) { |
158bd3ca TD |
528 | // we cannot work with a non-existant dialog, if you wish to |
529 | // load content via AJAX, see showAJAXDialog() instead | |
530 | if (!$.wcfIsset(dialogID)) return; | |
9f959ced | 531 | |
158bd3ca TD |
532 | var $dialog = $('#' + $.wcfEscapeID(dialogID)); |
533 | ||
36710781 | 534 | var dialogOptions = arguments[1] || {}; |
158bd3ca TD |
535 | $dialog.wcfDialog(dialogOptions); |
536 | }, | |
537 | ||
538 | /** | |
539 | * Returns a dynamically created id. | |
540 | * | |
541 | * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/dom/dom.js#L1789 | |
542 | * @return string | |
543 | */ | |
544 | getRandomID: function() { | |
545 | var $elementID = ''; | |
546 | ||
547 | do { | |
548 | $elementID = 'wcf' + this._idCounter++; | |
549 | } | |
550 | while ($.wcfIsset($elementID)); | |
551 | ||
552 | return $elementID; | |
553 | }, | |
554 | ||
555 | /** | |
556 | * Wrapper for $.inArray which returns boolean value instead of | |
557 | * index value, similar to PHP's in_array(). | |
558 | * | |
559 | * @param mixed needle | |
560 | * @param array haystack | |
561 | * @return boolean | |
562 | */ | |
563 | inArray: function(needle, haystack) { | |
564 | return ($.inArray(needle, haystack) != -1); | |
565 | }, | |
566 | ||
567 | /** | |
568 | * Adjusts effect for partially supported elements. | |
569 | * | |
570 | * @param object object | |
571 | * @param string effect | |
572 | * @return string | |
573 | */ | |
574 | getEffect: function(tagName, effect) { | |
575 | // most effects are not properly supported on table rows, use highlight instead | |
576 | if (tagName == 'tr') { | |
577 | return 'highlight'; | |
578 | } | |
579 | ||
580 | return effect; | |
581 | } | |
582 | }); | |
583 | ||
0d6ea23f | 584 | /** |
184a8d6d AE |
585 | * Dropdown API |
586 | */ | |
587 | WCF.Dropdown = { | |
588 | /** | |
589 | * list of callbacks | |
590 | * @var object | |
591 | */ | |
592 | _callbacks: { }, | |
593 | ||
594 | /** | |
595 | * initialization state | |
596 | * @var boolean | |
597 | */ | |
598 | _didInit: false, | |
599 | ||
600 | /** | |
601 | * list of registered dropdowns | |
602 | * @var object | |
603 | */ | |
604 | _dropdowns: { }, | |
605 | ||
606 | /** | |
607 | * Initializes dropdowns. | |
608 | */ | |
609 | init: function() { | |
7382b20f | 610 | var $userPanelHeight = $('#topMenu').outerHeight(); |
184a8d6d | 611 | var self = this; |
8f13d83e AE |
612 | $('.dropdownToggle').each(function(index, button) { |
613 | var $button = $(button); | |
614 | if ($button.data('target')) { | |
615 | return true; | |
616 | } | |
184a8d6d | 617 | |
8f13d83e AE |
618 | var $dropdown = $button.parents('.dropdown'); |
619 | if (!$dropdown.length) { | |
620 | // broken dropdown, ignore | |
184a8d6d AE |
621 | return true; |
622 | } | |
623 | ||
8f13d83e | 624 | var $containerID = $dropdown.wcfIdentify(); |
184a8d6d | 625 | if (!self._dropdowns[$containerID]) { |
8f13d83e AE |
626 | $button.click($.proxy(self._toggle, self)); |
627 | self._dropdowns[$containerID] = $dropdown; | |
184a8d6d | 628 | |
7382b20f AE |
629 | var $dropdownHeight = $dropdown.outerHeight(); |
630 | var $top = $dropdownHeight + 7; | |
631 | if ($dropdown.parents('#topMenu').length) { | |
632 | // fix calculation for user panel (elements may be shorter than they appear) | |
633 | $top = $userPanelHeight; | |
634 | } | |
635 | ||
184a8d6d | 636 | // calculate top offset for menu |
8f13d83e | 637 | $button.next('.dropdownMenu').css({ |
7382b20f | 638 | top: $top + 'px' |
184a8d6d AE |
639 | }); |
640 | } | |
8f13d83e AE |
641 | |
642 | $button.data('target', $containerID); | |
184a8d6d AE |
643 | }); |
644 | ||
645 | if (!this._didInit) { | |
646 | this._didInit = true; | |
647 | ||
648 | WCF.CloseOverlayHandler.addCallback('WCF.Dropdown', $.proxy(this._closeAll, this)); | |
649 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Dropdown', $.proxy(this.init, this)); | |
650 | } | |
651 | }, | |
652 | ||
653 | /** | |
654 | * Registers a callback notified upon dropdown state change. | |
655 | * | |
656 | * @param string identifier | |
657 | * @var object callback | |
658 | */ | |
659 | registerCallback: function(identifier, callback) { | |
660 | if (!$.isFunction(callback)) { | |
661 | console.debug("[WCF.Dropdown] Callback for '" + identifier + "' is invalid"); | |
662 | return false; | |
663 | } | |
664 | ||
665 | if (!this._callbacks[identifier]) { | |
666 | this._callbacks[identifier] = [ ]; | |
667 | } | |
668 | ||
669 | this._callbacks[identifier].push(callback); | |
670 | }, | |
671 | ||
672 | /** | |
673 | * Toggles a dropdown. | |
674 | * | |
675 | * @param object event | |
676 | */ | |
677 | _toggle: function(event) { | |
8f13d83e | 678 | var $targetID = $(event.currentTarget).data('target'); |
2b8f0792 AE |
679 | |
680 | // close all dropdowns | |
681 | for (var $containerID in this._dropdowns) { | |
682 | var $dropdown = this._dropdowns[$containerID]; | |
683 | if ($dropdown.hasClass('dropdownOpen')) { | |
684 | $dropdown.removeClass('dropdownOpen'); | |
685 | this._notifyCallbacks($dropdown, 'close'); | |
686 | } | |
687 | else if ($containerID === $targetID) { | |
8f13d83e AE |
688 | // fix top offset |
689 | var $dropdownMenu = $dropdown.find('.dropdownMenu'); | |
690 | if ($dropdownMenu.css('top') === '7px') { | |
691 | $dropdownMenu.css({ | |
692 | top: $dropdown.outerHeight() + 7 | |
693 | }); | |
694 | } | |
695 | ||
2b8f0792 AE |
696 | $dropdown.addClass('dropdownOpen'); |
697 | this._notifyCallbacks($dropdown, 'open'); | |
71662ae8 AE |
698 | |
699 | this.setAlignment($dropdown); | |
2b8f0792 | 700 | } |
184a8d6d AE |
701 | } |
702 | ||
703 | event.stopPropagation(); | |
704 | return false; | |
705 | }, | |
706 | ||
71662ae8 AE |
707 | /** |
708 | * Sets alignment for dropdown. | |
709 | * | |
710 | * @param jQuery dropdown | |
711 | * @param jQuery dropdownMenu | |
712 | */ | |
713 | setAlignment: function(dropdown, dropdownMenu) { | |
a63d4f4d | 714 | var $dropdownMenu = (dropdown) ? dropdown.find('.dropdownMenu:eq(0)') : dropdownMenu; |
71662ae8 AE |
715 | |
716 | // calculate if dropdown should be right-aligned if there is not enough space | |
717 | var $dimensions = $dropdownMenu.getDimensions('outer'); | |
718 | var $offsets = $dropdownMenu.getOffsets('offset'); | |
719 | var $windowWidth = $(window).width(); | |
720 | ||
721 | if (($offsets.left + $dimensions.width) > $windowWidth) { | |
722 | $dropdownMenu.css({ | |
723 | left: 'auto', | |
724 | right: '0px' | |
725 | }).addClass('dropdownArrowRight'); | |
726 | } | |
a63d4f4d | 727 | else if ($dropdownMenu.css('right') != '0px') { |
71662ae8 AE |
728 | $dropdownMenu.css({ |
729 | left: '0px', | |
730 | right: 'auto' | |
731 | }).removeClass('dropdownArrowRight'); | |
732 | } | |
733 | }, | |
734 | ||
184a8d6d AE |
735 | /** |
736 | * Closes all dropdowns. | |
737 | */ | |
738 | _closeAll: function() { | |
739 | for (var $containerID in this._dropdowns) { | |
740 | var $dropdown = this._dropdowns[$containerID]; | |
741 | if ($dropdown.hasClass('dropdownOpen')) { | |
742 | $dropdown.removeClass('dropdownOpen'); | |
743 | ||
744 | this._notifyCallbacks($dropdown, 'close'); | |
745 | } | |
746 | } | |
747 | }, | |
748 | ||
749 | /** | |
750 | * Closes a dropdown without notifying callbacks. | |
751 | * | |
752 | * @param string containerID | |
753 | */ | |
754 | close: function(containerID) { | |
755 | if (!this._dropdowns[containerID]) { | |
756 | return; | |
757 | } | |
758 | ||
759 | this._dropdowns[containerID].removeClass('open'); | |
760 | }, | |
761 | ||
762 | /** | |
763 | * Notifies callbacks. | |
764 | * | |
765 | * @param jQuery dropdown | |
766 | * @param string action | |
767 | */ | |
768 | _notifyCallbacks: function(dropdown, action) { | |
769 | var $containerID = dropdown.wcfIdentify(); | |
770 | if (!this._callbacks[$containerID]) { | |
771 | return; | |
772 | } | |
773 | ||
774 | for (var $i = 0, $length = this._callbacks[$containerID].length; $i < $length; $i++) { | |
775 | this._callbacks[$containerID][$i](dropdown, action); | |
776 | } | |
777 | } | |
778 | }; | |
5be96f42 | 779 | |
184a8d6d | 780 | /** |
0d6ea23f AE |
781 | * Clipboard API |
782 | */ | |
783 | WCF.Clipboard = { | |
784 | /** | |
785 | * action proxy object | |
786 | * @var WCF.Action.Proxy | |
787 | */ | |
788 | _actionProxy: null, | |
789 | ||
da27d58a MS |
790 | /** |
791 | * action objects | |
792 | * @var object | |
793 | */ | |
794 | _actionObjects: {}, | |
795 | ||
d322363b AE |
796 | /** |
797 | * list of clipboard containers | |
798 | * @var jQuery | |
799 | */ | |
800 | _container: null, | |
801 | ||
d893cd14 AE |
802 | /** |
803 | * container meta data | |
804 | * @var object | |
805 | */ | |
806 | _containerData: { }, | |
807 | ||
d322363b AE |
808 | /** |
809 | * user has marked items | |
810 | * @var boolean | |
811 | */ | |
812 | _hasMarkedItems: false, | |
813 | ||
a009a689 MS |
814 | /** |
815 | * list of ids of marked objects | |
816 | * @var array | |
817 | */ | |
818 | _markedObjectIDs: [], | |
819 | ||
0d6ea23f AE |
820 | /** |
821 | * current page | |
822 | * @var string | |
823 | */ | |
824 | _page: '', | |
825 | ||
826 | /** | |
827 | * proxy object | |
828 | * @var WCF.Action.Proxy | |
829 | */ | |
830 | _proxy: null, | |
831 | ||
f892f868 AE |
832 | /** |
833 | * list of elements already tracked for clipboard actions | |
834 | * @var object | |
835 | */ | |
836 | _trackedElements: { }, | |
837 | ||
0d6ea23f AE |
838 | /** |
839 | * Initializes the clipboard API. | |
840 | */ | |
da27d58a | 841 | init: function(page, hasMarkedItems, actionObjects) { |
0d6ea23f | 842 | this._page = page; |
da27d58a MS |
843 | this._actionObjects = actionObjects; |
844 | if (!actionObjects) { | |
845 | this._actionObjects = {}; | |
846 | } | |
847 | if (hasMarkedItems) { | |
848 | this._hasMarkedItems = true; | |
849 | } | |
0d6ea23f AE |
850 | |
851 | this._actionProxy = new WCF.Action.Proxy({ | |
852 | success: $.proxy(this._actionSuccess, this), | |
d71e5a29 | 853 | url: 'index.php/ClipboardProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND |
0d6ea23f AE |
854 | }); |
855 | ||
856 | this._proxy = new WCF.Action.Proxy({ | |
857 | success: $.proxy(this._success, this), | |
d71e5a29 | 858 | url: 'index.php/Clipboard/?t=' + SECURITY_TOKEN + SID_ARG_2ND |
0d6ea23f AE |
859 | }); |
860 | ||
861 | // init containers first | |
6f475a52 | 862 | this._containers = $('.jsClipboardContainer').each($.proxy(function(index, container) { |
0d6ea23f AE |
863 | this._initContainer(container); |
864 | }, this)); | |
d322363b AE |
865 | |
866 | // loads marked items | |
867 | if (this._hasMarkedItems) { | |
868 | this._loadMarkedItems(); | |
869 | } | |
f892f868 AE |
870 | |
871 | var self = this; | |
872 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Clipboard', function() { | |
873 | self._containers = $('.jsClipboardContainer').each($.proxy(function(index, container) { | |
874 | self._initContainer(container); | |
875 | }, self)); | |
876 | }); | |
d322363b AE |
877 | }, |
878 | ||
879 | /** | |
880 | * Loads marked items on init. | |
881 | */ | |
882 | _loadMarkedItems: function() { | |
883 | new WCF.Action.Proxy({ | |
884 | autoSend: true, | |
885 | data: { | |
d893cd14 | 886 | containerData: this._containerData, |
d322363b AE |
887 | pageClassName: this._page |
888 | }, | |
889 | success: $.proxy(this._loadMarkedItemsSuccess, this), | |
d71e5a29 | 890 | url: 'index.php/ClipboardLoadMarkedItems/?t=' + SECURITY_TOKEN + SID_ARG_2ND |
d322363b AE |
891 | }); |
892 | }, | |
893 | ||
92a6d759 AE |
894 | /** |
895 | * Reloads the list of marked items. | |
896 | */ | |
897 | reload: function() { | |
898 | this._loadMarkedItems(); | |
899 | }, | |
900 | ||
d322363b AE |
901 | /** |
902 | * Marks all returned items as marked | |
903 | * | |
904 | * @param object data | |
905 | * @param string textStatus | |
906 | * @param jQuery jqXHR | |
907 | */ | |
908 | _loadMarkedItemsSuccess: function(data, textStatus, jqXHR) { | |
91b5b09f AE |
909 | this._resetMarkings(); |
910 | ||
d322363b AE |
911 | for (var $typeName in data.markedItems) { |
912 | var $objectData = data.markedItems[$typeName]; | |
d322363b | 913 | for (var $i in $objectData) { |
a009a689 | 914 | this._markedObjectIDs.push($objectData[$i]); |
d322363b AE |
915 | } |
916 | ||
917 | // loop through all containers | |
a009a689 | 918 | this._containers.each($.proxy(function(index, container) { |
d322363b AE |
919 | var $container = $(container); |
920 | ||
921 | // typeName does not match, continue | |
922 | if ($container.data('type') != $typeName) { | |
923 | return true; | |
924 | } | |
925 | ||
926 | // mark items as marked | |
a009a689 | 927 | $container.find('input.jsClipboardItem').each($.proxy(function(innerIndex, item) { |
d322363b | 928 | var $item = $(item); |
a009a689 | 929 | if (WCF.inArray($item.data('objectID'), this._markedObjectIDs)) { |
d322363b | 930 | $item.attr('checked', 'checked'); |
483aaa32 AE |
931 | |
932 | // add marked class for element container | |
933 | $item.parents('.jsClipboardObject').addClass('jsMarked'); | |
d322363b | 934 | } |
a009a689 | 935 | }, this)); |
d322363b AE |
936 | |
937 | // check if there is a markAll-checkbox | |
6f475a52 | 938 | $container.find('input.jsClipboardMarkAll').each(function(innerIndex, markAll) { |
d322363b AE |
939 | var $allItemsMarked = true; |
940 | ||
6f475a52 | 941 | $container.find('input.jsClipboardItem').each(function(itemIndex, item) { |
d322363b AE |
942 | var $item = $(item); |
943 | if (!$item.attr('checked')) { | |
944 | $allItemsMarked = false; | |
945 | } | |
946 | }); | |
947 | ||
948 | if ($allItemsMarked) { | |
949 | $(markAll).attr('checked', 'checked'); | |
950 | } | |
951 | }); | |
a009a689 | 952 | }, this)); |
d322363b AE |
953 | } |
954 | ||
955 | // call success method to build item list editors | |
956 | this._success(data, textStatus, jqXHR); | |
0d6ea23f AE |
957 | }, |
958 | ||
91b5b09f AE |
959 | /** |
960 | * Resets all checkboxes. | |
961 | */ | |
962 | _resetMarkings: function() { | |
963 | this._containers.each(function(index, container) { | |
964 | var $container = $(container); | |
965 | ||
966 | $container.find('input.jsClipboardItem, input.jsClipboardMarkAll').removeAttr('checked'); | |
483aaa32 | 967 | $container.find('.jsClipboardObject').removeClass('jsMarked'); |
91b5b09f AE |
968 | }); |
969 | }, | |
970 | ||
0d6ea23f AE |
971 | /** |
972 | * Initializes a clipboard container. | |
973 | * | |
974 | * @param object container | |
975 | */ | |
976 | _initContainer: function(container) { | |
977 | var $container = $(container); | |
d893cd14 | 978 | var $containerID = $container.wcfIdentify(); |
0d6ea23f | 979 | |
f892f868 AE |
980 | if (!this._trackedElements[$containerID]) { |
981 | $container.find('.jsClipboardMarkAll').data('hasContainer', $containerID).click($.proxy(this._markAll, this)); | |
982 | ||
983 | this._containerData[$container.data('type')] = {}; | |
984 | $.each($container.data(), $.proxy(function(index, element) { | |
985 | if (index.match(/^type(.+)/)) { | |
986 | this._containerData[$container.data('type')][WCF.String.lcfirst(index.replace(/^type/, ''))] = element; | |
987 | } | |
988 | }, this)); | |
989 | ||
990 | this._trackedElements[$containerID] = [ ]; | |
991 | } | |
0d6ea23f | 992 | |
f892f868 AE |
993 | // track individual checkboxes |
994 | $container.find('input.jsClipboardItem').each($.proxy(function(index, input) { | |
995 | var $input = $(input); | |
996 | var $inputID = $input.wcfIdentify(); | |
997 | ||
998 | if (!WCF.inArray($inputID, this._trackedElements[$containerID])) { | |
999 | this._trackedElements[$containerID].push($inputID); | |
1000 | ||
1001 | $input.data('hasContainer', $containerID).click($.proxy(this._click, this)); | |
995afaf1 MS |
1002 | } |
1003 | }, this)); | |
0d6ea23f AE |
1004 | }, |
1005 | ||
1006 | /** | |
1007 | * Processes change checkbox state. | |
1008 | * | |
1009 | * @param object event | |
1010 | */ | |
1011 | _click: function(event) { | |
1012 | var $item = $(event.target); | |
1013 | var $objectID = $item.data('objectID'); | |
1014 | var $isMarked = ($item.attr('checked')) ? true : false; | |
1015 | var $objectIDs = [ $objectID ]; | |
1016 | ||
a009a689 MS |
1017 | if ($isMarked) { |
1018 | this._markedObjectIDs.push($objectID); | |
483aaa32 | 1019 | $item.parents('.jsClipboardObject').addClass('jsMarked'); |
a009a689 MS |
1020 | } |
1021 | else { | |
1022 | this._markedObjectIDs = $.removeArrayValue(this._markedObjectIDs, $objectID); | |
483aaa32 | 1023 | $item.parents('.jsClipboardObject').removeClass('jsMarked'); |
a009a689 MS |
1024 | } |
1025 | ||
0d6ea23f AE |
1026 | // item is part of a container |
1027 | if ($item.data('hasContainer')) { | |
1028 | var $container = $('#' + $item.data('hasContainer')); | |
1029 | var $type = $container.data('type'); | |
1030 | ||
1031 | // check if all items are marked | |
1032 | var $markedAll = true; | |
6f475a52 | 1033 | $container.find('input.jsClipboardItem').each(function(index, containerItem) { |
0d6ea23f AE |
1034 | var $containerItem = $(containerItem); |
1035 | if (!$containerItem.attr('checked')) { | |
1036 | $markedAll = false; | |
1037 | } | |
1038 | }); | |
1039 | ||
1040 | // simulate a ticked 'markAll' checkbox | |
6f475a52 | 1041 | $container.find('.jsClipboardMarkAll').each(function(index, markAll) { |
0d6ea23f AE |
1042 | if ($markedAll) { |
1043 | $(markAll).attr('checked', 'checked'); | |
1044 | } | |
1045 | else { | |
1046 | $(markAll).removeAttr('checked'); | |
1047 | } | |
1048 | }); | |
1049 | } | |
1050 | else { | |
1051 | // standalone item | |
1052 | var $type = $item.data('type'); | |
1053 | } | |
1054 | ||
1055 | this._saveState($type, $objectIDs, $isMarked); | |
1056 | }, | |
1057 | ||
1058 | /** | |
1059 | * Marks all associated clipboard items as checked. | |
1060 | * | |
1061 | * @param object event | |
1062 | */ | |
1063 | _markAll: function(event) { | |
1064 | var $item = $(event.target); | |
1065 | var $objectIDs = [ ]; | |
1066 | var $isMarked = true; | |
1067 | ||
1068 | // if markAll object is a checkbox, allow toggling | |
1069 | if ($item.getTagName() == 'input') { | |
1070 | $isMarked = $item.attr('checked'); | |
1071 | } | |
1072 | ||
1073 | // handle item containers | |
1074 | if ($item.data('hasContainer')) { | |
1075 | var $container = $('#' + $item.data('hasContainer')); | |
1076 | var $type = $container.data('type'); | |
1077 | ||
1078 | // toggle state for all associated items | |
a009a689 | 1079 | $container.find('input.jsClipboardItem').each($.proxy(function(index, containerItem) { |
0d6ea23f | 1080 | var $containerItem = $(containerItem); |
a009a689 | 1081 | var $objectID = $containerItem.data('objectID'); |
0d6ea23f AE |
1082 | if ($isMarked) { |
1083 | if (!$containerItem.attr('checked')) { | |
1084 | $containerItem.attr('checked', 'checked'); | |
a009a689 MS |
1085 | this._markedObjectIDs.push($objectID); |
1086 | $objectIDs.push($objectID); | |
0d6ea23f AE |
1087 | } |
1088 | } | |
1089 | else { | |
1090 | if ($containerItem.attr('checked')) { | |
1091 | $containerItem.removeAttr('checked'); | |
a009a689 MS |
1092 | this._markedObjectIDs = $.removeArrayValue(this._markedObjectIDs, $objectID); |
1093 | $objectIDs.push($objectID); | |
0d6ea23f AE |
1094 | } |
1095 | } | |
a009a689 | 1096 | }, this)); |
483aaa32 AE |
1097 | |
1098 | if ($isMarked) { | |
1099 | $container.find('.jsClipboardObject').addClass('jsMarked'); | |
1100 | } | |
1101 | else { | |
1102 | $container.find('.jsClipboardObject').removeClass('jsMarked'); | |
1103 | } | |
0d6ea23f AE |
1104 | } |
1105 | ||
1106 | // save new status | |
1107 | this._saveState($type, $objectIDs, $isMarked); | |
1108 | }, | |
1109 | ||
1110 | /** | |
1111 | * Saves clipboard item state. | |
1112 | * | |
1113 | * @param string type | |
1114 | * @param array objectIDs | |
1115 | * @param boolean isMarked | |
1116 | */ | |
1117 | _saveState: function(type, objectIDs, isMarked) { | |
1118 | this._proxy.setOption('data', { | |
1119 | action: (isMarked) ? 'mark' : 'unmark', | |
75a93a59 | 1120 | containerData: this._containerData, |
0d6ea23f AE |
1121 | objectIDs: objectIDs, |
1122 | pageClassName: this._page, | |
1123 | type: type | |
f4126129 | 1124 | }); |
0d6ea23f AE |
1125 | this._proxy.sendRequest(); |
1126 | }, | |
1127 | ||
1128 | /** | |
1129 | * Updates editor options. | |
1130 | * | |
1131 | * @param object data | |
1132 | * @param string textStatus | |
1133 | * @param jQuery jqXHR | |
1134 | */ | |
1135 | _success: function(data, textStatus, jqXHR) { | |
1136 | // clear all editors first | |
1137 | var $containers = {}; | |
6f475a52 | 1138 | $('.jsClipboardEditor').each(function(index, container) { |
0d6ea23f | 1139 | var $container = $(container); |
1b4e9186 AE |
1140 | var $types = eval($container.data('types')); |
1141 | for (var $i = 0, $length = $types.length; $i < $length; $i++) { | |
1142 | var $typeName = $types[$i]; | |
0d6ea23f AE |
1143 | $containers[$typeName] = $container; |
1144 | } | |
1145 | ||
b3991cb3 AE |
1146 | var $containerID = $container.wcfIdentify(); |
1147 | WCF.CloseOverlayHandler.removeCallback($containerID); | |
1148 | ||
0d6ea23f AE |
1149 | $container.empty(); |
1150 | }); | |
1151 | ||
8f3284a3 | 1152 | // do not build new editors |
0d6ea23f AE |
1153 | if (!data.items) return; |
1154 | ||
1155 | // rebuild editors | |
1156 | for (var $typeName in data.items) { | |
1157 | if (!$containers[$typeName]) { | |
1158 | continue; | |
1159 | } | |
1160 | ||
1161 | // create container | |
1162 | var $container = $containers[$typeName]; | |
1b4e9186 AE |
1163 | var $list = $container.children('ul'); |
1164 | if ($list.length == 0) { | |
844e6560 | 1165 | $list = $('<ul class="dropdown"></ul>').appendTo($container); |
1b4e9186 AE |
1166 | } |
1167 | ||
0d6ea23f | 1168 | var $editor = data.items[$typeName]; |
844e6560 AE |
1169 | var $label = $('<li><span class="dropdownToggle button">' + $editor.label + '</span></li>').appendTo($list); |
1170 | var $itemList = $('<ol class="dropdownMenu"></ol>').appendTo($label); | |
1171 | ||
1172 | $label.click(function() { $list.toggleClass('dropdownOpen'); }); | |
0d6ea23f AE |
1173 | |
1174 | // create editor items | |
1175 | for (var $itemIndex in $editor.items) { | |
1176 | var $item = $editor.items[$itemIndex]; | |
49c164a8 AE |
1177 | |
1178 | if ($item.actionName === 'unmarkAll') { | |
1179 | $('<li class="dropdownDivider" />').appendTo($itemList); | |
1180 | } | |
1181 | ||
844e6560 | 1182 | var $listItem = $('<li><span>' + $item.label + '</span></li>').appendTo($itemList); |
da27d58a | 1183 | $listItem.data('objectType', $typeName); |
0d6ea23f AE |
1184 | $listItem.data('actionName', $item.actionName).data('parameters', $item.parameters); |
1185 | $listItem.data('internalData', $item.internalData).data('url', $item.url).data('type', $typeName); | |
1186 | ||
1187 | // bind event | |
1188 | $listItem.click($.proxy(this._executeAction, this)); | |
1189 | } | |
9f959ced | 1190 | |
b3991cb3 AE |
1191 | // block click event |
1192 | $container.click(function(event) { | |
1193 | event.stopPropagation(); | |
1194 | }); | |
9f959ced | 1195 | |
b3991cb3 AE |
1196 | // register event handler |
1197 | var $containerID = $container.wcfIdentify(); | |
1198 | WCF.CloseOverlayHandler.addCallback($containerID, $.proxy(this._closeLists, this)); | |
0d6ea23f AE |
1199 | } |
1200 | }, | |
da27d58a MS |
1201 | |
1202 | /** | |
1203 | * Closes the clipboard editor item list. | |
1204 | */ | |
b3991cb3 | 1205 | _closeLists: function() { |
d27e2667 | 1206 | $('.jsClipboardEditor ul').removeClass('dropdownOpen'); |
b3991cb3 | 1207 | }, |
0d6ea23f AE |
1208 | |
1209 | /** | |
1210 | * Executes a clipboard editor item action. | |
1211 | * | |
1212 | * @param object event | |
1213 | */ | |
1214 | _executeAction: function(event) { | |
9ce295a0 | 1215 | var $listItem = $(event.currentTarget); |
0d6ea23f AE |
1216 | var $url = $listItem.data('url'); |
1217 | if ($url) { | |
1218 | window.location.href = $url; | |
1219 | } | |
1220 | ||
49c164a8 AE |
1221 | if ($listItem.data('parameters').className && $listItem.data('parameters').actionName) { |
1222 | if ($listItem.data('parameters').actionName === 'unmarkAll' || $listItem.data('parameters').objectIDs) { | |
1223 | var $confirmMessage = $listItem.data('internalData')['confirmMessage']; | |
1224 | if ($confirmMessage) { | |
1225 | var $template = $listItem.data('internalData')['template']; | |
1226 | if ($template) $template = $($template); | |
1227 | ||
1228 | WCF.System.Confirmation.show($confirmMessage, $.proxy(function(action) { | |
1229 | if (action === 'confirm') { | |
1230 | var $data = { }; | |
1231 | ||
1232 | if ($template && $template.length) { | |
1233 | $('#wcfSystemConfirmationContent').find('input, select, textarea').each(function(index, item) { | |
1234 | var $item = $(item); | |
1235 | $data[$item.prop('name')] = $item.val(); | |
1236 | }); | |
1237 | } | |
1238 | ||
1239 | this._executeAJAXActions($listItem, $data); | |
db6b20b8 | 1240 | } |
49c164a8 AE |
1241 | }, this), '', $template); |
1242 | } | |
1243 | else { | |
1244 | this._executeAJAXActions($listItem, { }); | |
1245 | } | |
da27d58a | 1246 | } |
a009a689 MS |
1247 | } |
1248 | ||
0d6ea23f | 1249 | // fire event |
a009a689 | 1250 | $listItem.trigger('clipboardAction', [ $listItem.data('type'), $listItem.data('actionName'), $listItem.data('parameters') ]); |
0d6ea23f AE |
1251 | }, |
1252 | ||
da27d58a MS |
1253 | /** |
1254 | * Executes the AJAX actions for the given editor list item. | |
1255 | * | |
1256 | * @param jQuery listItem | |
db6b20b8 | 1257 | * @param object data |
da27d58a | 1258 | */ |
db6b20b8 AE |
1259 | _executeAJAXActions: function(listItem, data) { |
1260 | data = data || { }; | |
1261 | var $objectIDs = []; | |
49c164a8 AE |
1262 | if (listItem.data('parameters').actionName !== 'unmarkAll') { |
1263 | $.each(listItem.data('parameters').objectIDs, function(index, objectID) { | |
1264 | $objectIDs.push(parseInt(objectID)); | |
1265 | }); | |
1266 | } | |
db6b20b8 | 1267 | |
54c91463 | 1268 | var $parameters = { |
438b4f4d MS |
1269 | data: data, |
1270 | containerData: this._containerData[listItem.data('type')] | |
54c91463 | 1271 | }; |
faa1fd02 | 1272 | var $__parameters = listItem.data('internalData')['parameters']; |
54c91463 AE |
1273 | if ($__parameters !== undefined) { |
1274 | for (var $key in $__parameters) { | |
1275 | $parameters[$key] = $__parameters[$key]; | |
1276 | } | |
1277 | } | |
1278 | ||
da27d58a MS |
1279 | new WCF.Action.Proxy({ |
1280 | autoSend: true, | |
1281 | data: { | |
1282 | actionName: listItem.data('parameters').actionName, | |
1283 | className: listItem.data('parameters').className, | |
db6b20b8 | 1284 | objectIDs: $objectIDs, |
54c91463 | 1285 | parameters: $parameters |
da27d58a | 1286 | }, |
1ecfbb69 | 1287 | success: $.proxy(function(data) { |
49c164a8 AE |
1288 | if (listItem.data('parameters').actionName !== 'unmarkAll') { |
1289 | listItem.trigger('clipboardActionResponse', [ data, listItem.data('type'), listItem.data('actionName'), listItem.data('parameters') ]); | |
1290 | } | |
1ecfbb69 AE |
1291 | |
1292 | this._loadMarkedItems(); | |
91b5b09f | 1293 | }, this) |
da27d58a MS |
1294 | }); |
1295 | ||
1296 | if (this._actionObjects[listItem.data('objectType')] && this._actionObjects[listItem.data('objectType')][listItem.data('parameters').actionName]) { | |
05710b5d | 1297 | this._actionObjects[listItem.data('objectType')][listItem.data('parameters').actionName].triggerEffect($objectIDs); |
da27d58a MS |
1298 | } |
1299 | }, | |
1300 | ||
0d6ea23f AE |
1301 | /** |
1302 | * Sends a clipboard proxy request. | |
1303 | * | |
1304 | * @param object item | |
1305 | */ | |
1306 | sendRequest: function(item) { | |
1307 | var $item = $(item); | |
1308 | ||
1309 | this._actionProxy.setOption('data', { | |
1310 | parameters: $item.data('parameters'), | |
1311 | typeName: $item.data('type') | |
1312 | }); | |
1313 | this._actionProxy.sendRequest(); | |
1314 | } | |
1315 | }; | |
1316 | ||
158bd3ca TD |
1317 | /** |
1318 | * Provides a simple call for periodical executed functions. Based upon | |
1319 | * ideas by Prototype's PeriodicalExecuter. | |
1320 | * | |
1321 | * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/periodical_executer.js | |
1322 | * @param function callback | |
1323 | * @param integer delay | |
1324 | */ | |
39e27190 | 1325 | WCF.PeriodicalExecuter = Class.extend({ |
ac37b8fe AE |
1326 | /** |
1327 | * callback for each execution cycle | |
1328 | * @var object | |
1329 | */ | |
1330 | _callback: null, | |
1331 | ||
1332 | /** | |
1333 | * interval id | |
9f959ced | 1334 | * @var integer |
ac37b8fe AE |
1335 | */ |
1336 | _intervalID: null, | |
1337 | ||
1338 | /** | |
1339 | * execution state | |
1340 | * @var boolean | |
1341 | */ | |
1342 | _isExecuting: false, | |
1343 | ||
158bd3ca TD |
1344 | /** |
1345 | * Initializes a periodical executer. | |
1346 | * | |
1347 | * @param function callback | |
1348 | * @param integer delay | |
1349 | */ | |
1350 | init: function(callback, delay) { | |
ac37b8fe AE |
1351 | if (!$.isFunction(callback)) { |
1352 | console.debug('[WCF.PeriodicalExecuter] Given callback is invalid, aborting.'); | |
1353 | return; | |
1354 | } | |
158bd3ca | 1355 | |
ac37b8fe AE |
1356 | this._callback = callback; |
1357 | this._intervalID = setInterval($.proxy(this._execute, this), delay); | |
158bd3ca TD |
1358 | }, |
1359 | ||
1360 | /** | |
1361 | * Executes callback. | |
1362 | */ | |
1363 | _execute: function() { | |
ac37b8fe AE |
1364 | if (!this._isExecuting) { |
1365 | try { | |
1366 | this._isExecuting = true; | |
1367 | this._callback(this); | |
1368 | this._isExecuting = false; | |
1369 | } | |
1370 | catch (e) { | |
1371 | this._isExecuting = false; | |
1372 | throw e; | |
1373 | } | |
158bd3ca TD |
1374 | } |
1375 | }, | |
1376 | ||
1377 | /** | |
1378 | * Terminates loop. | |
1379 | */ | |
1380 | stop: function() { | |
ac37b8fe AE |
1381 | if (!this._intervalID) { |
1382 | return; | |
1383 | } | |
1384 | ||
1385 | clearInterval(this._intervalID); | |
158bd3ca | 1386 | } |
39e27190 | 1387 | }); |
158bd3ca TD |
1388 | |
1389 | /** | |
1390 | * Namespace for AJAXProxies | |
1391 | */ | |
1392 | WCF.Action = {}; | |
1393 | ||
1394 | /** | |
1395 | * Basic implementation for AJAX-based proxyies | |
1396 | * | |
1397 | * @param object options | |
1398 | */ | |
39e27190 | 1399 | WCF.Action.Proxy = Class.extend({ |
b3991cb3 AE |
1400 | /** |
1401 | * count of active requests | |
1402 | * @var integer | |
1403 | */ | |
1404 | _activeRequests: 0, | |
9f959ced | 1405 | |
b3991cb3 AE |
1406 | /** |
1407 | * loading overlay | |
1408 | * @var jQuery | |
1409 | */ | |
1410 | _loadingOverlay: null, | |
9f959ced | 1411 | |
b3991cb3 AE |
1412 | /** |
1413 | * loading overlay state | |
1414 | * @var boolean | |
1415 | */ | |
1416 | _loadingOverlayVisible: false, | |
9f959ced | 1417 | |
b3991cb3 AE |
1418 | /** |
1419 | * timer for overlay activity | |
1420 | * @var integer | |
1421 | */ | |
1422 | _loadingOverlayVisibleTimer: 0, | |
2f326e85 AE |
1423 | |
1424 | /** | |
1425 | * suppresses errors | |
1426 | * @var boolean | |
1427 | */ | |
1428 | _suppressErrors: false, | |
9f959ced | 1429 | |
158bd3ca TD |
1430 | /** |
1431 | * Initializes AJAXProxy. | |
1432 | * | |
1433 | * @param object options | |
1434 | */ | |
1435 | init: function(options) { | |
1436 | // initialize default values | |
1437 | this.options = $.extend(true, { | |
1438 | autoSend: false, | |
1439 | data: { }, | |
1440 | after: null, | |
1441 | init: null, | |
1442 | failure: null, | |
324e8301 | 1443 | showLoadingOverlay: true, |
158bd3ca TD |
1444 | success: null, |
1445 | type: 'POST', | |
d71e5a29 | 1446 | url: 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND |
158bd3ca TD |
1447 | }, options); |
1448 | ||
1449 | this.confirmationDialog = null; | |
1450 | this.loading = null; | |
2f326e85 | 1451 | this._suppressErrors = false; |
158bd3ca TD |
1452 | |
1453 | // send request immediately after initialization | |
1454 | if (this.options.autoSend) { | |
1455 | this.sendRequest(); | |
1456 | } | |
2f326e85 AE |
1457 | |
1458 | var self = this; | |
1459 | $(window).on('beforeunload', function() { self._suppressErrors = true; }); | |
158bd3ca TD |
1460 | }, |
1461 | ||
1462 | /** | |
1463 | * Sends an AJAX request. | |
1464 | */ | |
1465 | sendRequest: function() { | |
1466 | this._init(); | |
1467 | ||
1468 | $.ajax({ | |
1469 | data: this.options.data, | |
1470 | dataType: 'json', | |
1471 | type: this.options.type, | |
1472 | url: this.options.url, | |
1473 | success: $.proxy(this._success, this), | |
1474 | error: $.proxy(this._failure, this) | |
1475 | }); | |
1476 | }, | |
1477 | ||
1478 | /** | |
1479 | * Fires before request is send, displays global loading status. | |
1480 | */ | |
1481 | _init: function() { | |
1482 | if ($.isFunction(this.options.init)) { | |
06c820f0 | 1483 | this.options.init(this); |
158bd3ca TD |
1484 | } |
1485 | ||
350fad79 AE |
1486 | this._activeRequests++; |
1487 | ||
324e8301 | 1488 | if (this.options.showLoadingOverlay) { |
324e8301 AE |
1489 | this._showLoadingOverlay(); |
1490 | } | |
b3991cb3 | 1491 | }, |
9f959ced | 1492 | |
b3991cb3 AE |
1493 | /** |
1494 | * Displays the loading overlay if not already visible due to an active request. | |
1495 | */ | |
1496 | _showLoadingOverlay: function() { | |
1497 | // create loading overlay on first run | |
1498 | if (this._loadingOverlay === null) { | |
957810a6 | 1499 | this._loadingOverlay = $('<div class="spinner"><img src="' + WCF.Icon.get('wcf.icon.loading') + '" alt="" class="icon48" /> <span>' + WCF.Language.get('wcf.global.loading') + '</span></div>').hide().appendTo($('body')); |
b3991cb3 | 1500 | } |
9f959ced | 1501 | |
b3991cb3 AE |
1502 | // fade in overlay |
1503 | if (!this._loadingOverlayVisible) { | |
1504 | this._loadingOverlayVisible = true; | |
7fecfefc | 1505 | this._loadingOverlay.stop(true, true).fadeIn(100, $.proxy(function() { |
b3991cb3 AE |
1506 | new WCF.PeriodicalExecuter($.proxy(this._hideLoadingOverlay, this), 100); |
1507 | }, this)); | |
1508 | } | |
1509 | }, | |
9f959ced | 1510 | |
b3991cb3 AE |
1511 | /** |
1512 | * Hides loading overlay if no requests are active and the timer reached at least 1 second. | |
1513 | * | |
1514 | * @param object pe | |
1515 | */ | |
1516 | _hideLoadingOverlay: function(pe) { | |
1517 | this._loadingOverlayVisibleTimer += 100; | |
1518 | ||
7fecfefc | 1519 | if (this._activeRequests == 0 && this._loadingOverlayVisibleTimer >= 100) { |
b3991cb3 AE |
1520 | this._loadingOverlayVisible = false; |
1521 | this._loadingOverlayVisibleTimer = 0; | |
1522 | pe.stop(); | |
9f959ced | 1523 | |
7fecfefc | 1524 | this._loadingOverlay.fadeOut(100); |
b3991cb3 | 1525 | } |
158bd3ca TD |
1526 | }, |
1527 | ||
1528 | /** | |
1529 | * Handles AJAX errors. | |
1530 | * | |
1531 | * @param object jqXHR | |
1532 | * @param string textStatus | |
1533 | * @param string errorThrown | |
1534 | */ | |
1535 | _failure: function(jqXHR, textStatus, errorThrown) { | |
1536 | try { | |
1537 | var data = $.parseJSON(jqXHR.responseText); | |
1538 | ||
1539 | // call child method if applicable | |
2f326e85 | 1540 | var $showError = true; |
158bd3ca | 1541 | if ($.isFunction(this.options.failure)) { |
2f326e85 | 1542 | $showError = this.options.failure(jqXHR, textStatus, errorThrown, jqXHR.responseText); |
158bd3ca TD |
1543 | } |
1544 | ||
2f326e85 AE |
1545 | if (!this._suppressErrors && $showError !== false) { |
1546 | $('<div class="ajaxDebugMessage"><p>' + data.message + '</p><p>Stacktrace:</p><p>' + data.stacktrace + '</p></div>').wcfDialog({ title: WCF.Language.get('wcf.global.error.title') }); | |
1547 | } | |
158bd3ca TD |
1548 | } |
1549 | // failed to parse JSON | |
1550 | catch (e) { | |
2f326e85 AE |
1551 | // call child method if applicable |
1552 | var $showError = true; | |
1553 | if ($.isFunction(this.options.failure)) { | |
1554 | $showError = this.options.failure(jqXHR, textStatus, errorThrown, jqXHR.responseText); | |
1555 | } | |
1556 | ||
1557 | if (!this._suppressErrors && $showError !== false) { | |
1558 | $('<div class="ajaxDebugMessage"><p>' + jqXHR.responseText + '</p></div>').wcfDialog({ title: WCF.Language.get('wcf.global.error.title') }); | |
1559 | } | |
158bd3ca TD |
1560 | } |
1561 | ||
1562 | this._after(); | |
1563 | }, | |
1564 | ||
1565 | /** | |
1566 | * Handles successful AJAX requests. | |
1567 | * | |
1568 | * @param object data | |
1569 | * @param string textStatus | |
1570 | * @param object jqXHR | |
1571 | */ | |
1572 | _success: function(data, textStatus, jqXHR) { | |
597e2774 AE |
1573 | // enable DOMNodeInserted event |
1574 | WCF.DOMNodeInsertedHandler.enable(); | |
1575 | ||
158bd3ca TD |
1576 | // call child method if applicable |
1577 | if ($.isFunction(this.options.success)) { | |
1578 | this.options.success(data, textStatus, jqXHR); | |
1579 | } | |
1580 | ||
1581 | this._after(); | |
1582 | }, | |
1583 | ||
1584 | /** | |
1585 | * Fires after an AJAX request, hides global loading status. | |
1586 | */ | |
1587 | _after: function() { | |
1588 | if ($.isFunction(this.options.after)) { | |
1589 | this.options.after(); | |
1590 | } | |
350fad79 AE |
1591 | |
1592 | this._activeRequests--; | |
597e2774 AE |
1593 | |
1594 | // disable DOMNodeInserted event | |
1595 | WCF.DOMNodeInsertedHandler.disable(); | |
158bd3ca TD |
1596 | }, |
1597 | ||
1598 | /** | |
1599 | * Sets options, MUST be used to set parameters before sending request | |
1600 | * if calling from child classes. | |
1601 | * | |
1602 | * @param string optionName | |
1603 | * @param mixed optionData | |
1604 | */ | |
1605 | setOption: function(optionName, optionData) { | |
1606 | this.options[optionName] = optionData; | |
f2e420cc | 1607 | }, |
9f959ced | 1608 | |
f2e420cc AE |
1609 | /** |
1610 | * Displays a spinner image for given element. | |
1611 | * | |
1612 | * @param jQuery element | |
1613 | */ | |
1614 | showSpinner: function(element) { | |
1615 | element = $(element); | |
9f959ced | 1616 | |
f2e420cc | 1617 | if (element.getTagName() !== 'img') { |
6f475a52 | 1618 | console.debug('The given element is not an image, aborting.'); |
f2e420cc AE |
1619 | return; |
1620 | } | |
9f959ced | 1621 | |
f2e420cc AE |
1622 | // force element dimensions |
1623 | element.attr('width', element.attr('width')); | |
1624 | element.attr('height', element.attr('height')); | |
9f959ced | 1625 | |
f2e420cc | 1626 | // replace image |
856e2054 | 1627 | element.attr('src', WCF.Icon.get('wcf.global.loading')); |
158bd3ca | 1628 | } |
39e27190 | 1629 | }); |
158bd3ca TD |
1630 | |
1631 | /** | |
1632 | * Basic implementation for simple proxy access using bound elements. | |
1633 | * | |
1634 | * @param object options | |
1635 | * @param object callbacks | |
1636 | */ | |
39e27190 | 1637 | WCF.Action.SimpleProxy = Class.extend({ |
158bd3ca TD |
1638 | /** |
1639 | * Initializes SimpleProxy. | |
1640 | * | |
1641 | * @param object options | |
1642 | * @param object callbacks | |
1643 | */ | |
1644 | init: function(options, callbacks) { | |
1645 | /** | |
1646 | * action-specific options | |
1647 | */ | |
1648 | this.options = $.extend(true, { | |
1649 | action: '', | |
1650 | className: '', | |
1651 | elements: null, | |
1652 | eventName: 'click' | |
1653 | }, options); | |
1654 | ||
1655 | /** | |
1656 | * proxy-specific options | |
1657 | */ | |
1658 | this.callbacks = $.extend(true, { | |
1659 | after: null, | |
1660 | failure: null, | |
1661 | init: null, | |
1662 | success: null | |
1663 | }, callbacks); | |
1664 | ||
1665 | if (!this.options.elements) return; | |
1666 | ||
1667 | // initialize proxy | |
1668 | this.proxy = new WCF.Action.Proxy(this.callbacks); | |
1669 | ||
1670 | // bind event listener | |
1671 | this.options.elements.each($.proxy(function(index, element) { | |
1672 | $(element).bind(this.options.eventName, $.proxy(this._handleEvent, this)); | |
1673 | }, this)); | |
1674 | }, | |
1675 | ||
1676 | /** | |
1677 | * Handles event actions. | |
1678 | * | |
1679 | * @param object event | |
1680 | */ | |
1681 | _handleEvent: function(event) { | |
1682 | this.proxy.setOption('data', { | |
1683 | actionName: this.options.action, | |
1684 | className: this.options.className, | |
1685 | objectIDs: [ $(event.target).data('objectID') ] | |
1686 | }); | |
1687 | ||
1688 | this.proxy.sendRequest(); | |
1689 | } | |
39e27190 | 1690 | }); |
158bd3ca TD |
1691 | |
1692 | /** | |
1693 | * Basic implementation for AJAXProxy-based deletion. | |
1694 | * | |
1695 | * @param string className | |
d371330f | 1696 | * @param string containerSelector |
158bd3ca | 1697 | */ |
5ffc72b6 | 1698 | WCF.Action.Delete = Class.extend({ |
d371330f AE |
1699 | /** |
1700 | * action class name | |
1701 | * @var string | |
1702 | */ | |
1703 | _className: '', | |
1704 | ||
1705 | /** | |
1706 | * container selector | |
1707 | * @var string | |
1708 | */ | |
1709 | _containerSelector: '', | |
1710 | ||
1711 | /** | |
1712 | * list of known container ids | |
1713 | * @var array<string> | |
1714 | */ | |
1715 | _containers: [ ], | |
1716 | ||
158bd3ca TD |
1717 | /** |
1718 | * Initializes 'delete'-Proxy. | |
1719 | * | |
1720 | * @param string className | |
d371330f | 1721 | * @param string containerSelector |
158bd3ca | 1722 | */ |
d371330f AE |
1723 | init: function(className, containerSelector) { |
1724 | this._containerSelector = containerSelector; | |
1725 | this._className = className; | |
1726 | this.proxy = new WCF.Action.Proxy({ | |
158bd3ca | 1727 | success: $.proxy(this._success, this) |
d371330f | 1728 | }); |
158bd3ca | 1729 | |
d371330f AE |
1730 | this._initElements(); |
1731 | ||
1732 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Action.Delete' + this._className.hashCode(), $.proxy(this._initElements, this)); | |
1733 | }, | |
1734 | ||
1735 | /** | |
1736 | * Initializes available element containers. | |
1737 | */ | |
1738 | _initElements: function() { | |
1739 | var self = this; | |
1740 | $(this._containerSelector).each(function(index, container) { | |
1741 | var $container = $(container); | |
1742 | var $containerID = $container.wcfIdentify(); | |
1743 | ||
1744 | if (!WCF.inArray($containerID, self._containers)) { | |
1745 | self._containers.push($containerID); | |
1746 | $container.find('.jsDeleteButton').click($.proxy(self._click, self)); | |
1747 | } | |
1748 | }); | |
158bd3ca TD |
1749 | }, |
1750 | ||
1751 | /** | |
1752 | * Sends AJAX request. | |
1753 | * | |
1754 | * @param object event | |
1755 | */ | |
1756 | _click: function(event) { | |
eea52a29 | 1757 | var $target = $(event.currentTarget); |
158bd3ca TD |
1758 | |
1759 | if ($target.data('confirmMessage')) { | |
f02b3f75 | 1760 | WCF.System.Confirmation.show($target.data('confirmMessage'), $.proxy(this._execute, this), { target: $target }); |
158bd3ca TD |
1761 | } |
1762 | else { | |
f2e420cc | 1763 | this.proxy.showSpinner($target); |
158bd3ca TD |
1764 | this._sendRequest($target); |
1765 | } | |
f02b3f75 AE |
1766 | }, |
1767 | ||
1768 | /** | |
1769 | * Executes deletion. | |
1770 | * | |
1771 | * @param string action | |
1772 | * @param object parameters | |
1773 | */ | |
1774 | _execute: function(action, parameters) { | |
1775 | if (action === 'cancel') { | |
1776 | return; | |
1777 | } | |
158bd3ca | 1778 | |
f02b3f75 AE |
1779 | this.proxy.showSpinner(parameters.target); |
1780 | this._sendRequest(parameters.target); | |
158bd3ca TD |
1781 | }, |
1782 | ||
1783 | _sendRequest: function(object) { | |
1784 | this.proxy.setOption('data', { | |
1785 | actionName: 'delete', | |
d371330f | 1786 | className: this._className, |
158bd3ca TD |
1787 | objectIDs: [ $(object).data('objectID') ] |
1788 | }); | |
1789 | ||
1790 | this.proxy.sendRequest(); | |
1791 | }, | |
1792 | ||
1793 | /** | |
1794 | * Deletes items from containers. | |
1795 | * | |
1796 | * @param object data | |
1797 | * @param string textStatus | |
1798 | * @param object jqXHR | |
1799 | */ | |
1800 | _success: function(data, textStatus, jqXHR) { | |
da27d58a MS |
1801 | this.triggerEffect(data.objectIDs); |
1802 | }, | |
1803 | ||
1804 | /** | |
1805 | * Triggers the delete effect for the objects with the given ids. | |
1806 | * | |
1807 | * @param array objectIDs | |
1808 | */ | |
1809 | triggerEffect: function(objectIDs) { | |
d371330f AE |
1810 | for (var $index in this._containers) { |
1811 | var $container = $('#' + this._containers[$index]); | |
1812 | if (WCF.inArray($container.find('.jsDeleteButton').data('objectID'), objectIDs)) { | |
1813 | $container.wcfBlindOut('up', function() { $container.remove(); }); | |
158bd3ca | 1814 | } |
d371330f | 1815 | } |
158bd3ca | 1816 | } |
5ffc72b6 | 1817 | }); |
158bd3ca TD |
1818 | |
1819 | /** | |
1820 | * Basic implementation for AJAXProxy-based toggle actions. | |
1821 | * | |
1822 | * @param string className | |
1823 | * @param jQuery containerList | |
32a9e8a4 | 1824 | * @param string toggleButtonSelector |
158bd3ca | 1825 | */ |
5ffc72b6 | 1826 | WCF.Action.Toggle = Class.extend({ |
158bd3ca TD |
1827 | /** |
1828 | * Initializes 'toggle'-Proxy | |
1829 | * | |
1830 | * @param string className | |
1831 | * @param jQuery containerList | |
1832 | */ | |
32a9e8a4 | 1833 | init: function(className, containerList, toggleButtonSelector) { |
158bd3ca TD |
1834 | if (!containerList.length) return; |
1835 | this.containerList = containerList; | |
1836 | this.className = className; | |
1837 | ||
438f2be6 | 1838 | this.toggleButtonSelector = '.jsToggleButton'; |
32a9e8a4 MS |
1839 | if (toggleButtonSelector) { |
1840 | this.toggleButtonSelector = toggleButtonSelector; | |
1841 | } | |
1842 | ||
158bd3ca TD |
1843 | // initialize proxy |
1844 | var options = { | |
1845 | success: $.proxy(this._success, this) | |
1846 | }; | |
1847 | this.proxy = new WCF.Action.Proxy(options); | |
1848 | ||
1849 | // bind event listener | |
1850 | this.containerList.each($.proxy(function(index, container) { | |
32a9e8a4 | 1851 | $(container).find(this.toggleButtonSelector).bind('click', $.proxy(this._click, this)); |
158bd3ca TD |
1852 | }, this)); |
1853 | }, | |
1854 | ||
1855 | /** | |
1856 | * Sends AJAX request. | |
1857 | * | |
1858 | * @param object event | |
1859 | */ | |
1860 | _click: function(event) { | |
1861 | this.proxy.setOption('data', { | |
1862 | actionName: 'toggle', | |
1863 | className: this.className, | |
1864 | objectIDs: [ $(event.target).data('objectID') ] | |
1865 | }); | |
1866 | ||
1867 | this.proxy.sendRequest(); | |
1868 | }, | |
1869 | ||
1870 | /** | |
1871 | * Toggles status icons. | |
1872 | * | |
1873 | * @param object data | |
1874 | * @param string textStatus | |
1875 | * @param object jqXHR | |
1876 | */ | |
1877 | _success: function(data, textStatus, jqXHR) { | |
da27d58a MS |
1878 | this.triggerEffect(data.objectIDs); |
1879 | }, | |
1880 | ||
1881 | /** | |
1882 | * Triggers the toggle effect for the objects with the given ids. | |
1883 | * | |
1884 | * @param array objectIDs | |
1885 | */ | |
1886 | triggerEffect: function(objectIDs) { | |
32a9e8a4 MS |
1887 | this.containerList.each($.proxy(function(index, container) { |
1888 | var $toggleButton = $(container).find(this.toggleButtonSelector); | |
da27d58a | 1889 | if (WCF.inArray($toggleButton.data('objectID'), objectIDs)) { |
158bd3ca TD |
1890 | $(container).wcfHighlight(); |
1891 | ||
1892 | // toggle icon source | |
1893 | $toggleButton.attr('src', function() { | |
1fdaa516 MW |
1894 | if (this.src.match(/disabled\.svg$/)) { |
1895 | return this.src.replace(/disabled\.svg$/, 'enabled.svg'); | |
158bd3ca TD |
1896 | } |
1897 | else { | |
1fdaa516 | 1898 | return this.src.replace(/enabled\.svg$/, 'disabled.svg'); |
158bd3ca TD |
1899 | } |
1900 | }); | |
87e89400 | 1901 | |
158bd3ca TD |
1902 | // toogle icon title |
1903 | $toggleButton.attr('title', function() { | |
1fdaa516 | 1904 | if (this.src.match(/enabled\.svg$/)) { |
7c0999cd | 1905 | if ($(this).data('disableTitle')) { |
bc485da5 | 1906 | return $(this).data('disableTitle'); |
7c0999cd MS |
1907 | } |
1908 | ||
1909 | return WCF.Language.get('wcf.global.button.disable'); | |
158bd3ca TD |
1910 | } |
1911 | else { | |
7c0999cd | 1912 | if ($(this).data('enableTitle')) { |
bc485da5 | 1913 | return $(this).data('enableTitle'); |
7c0999cd MS |
1914 | } |
1915 | ||
1916 | return WCF.Language.get('wcf.global.button.enable'); | |
158bd3ca TD |
1917 | } |
1918 | }); | |
87e89400 MS |
1919 | |
1920 | // toggle css class | |
1921 | $(container).toggleClass('disabled'); | |
158bd3ca | 1922 | } |
32a9e8a4 | 1923 | }, this)); |
158bd3ca | 1924 | } |
5ffc72b6 | 1925 | }); |
158bd3ca | 1926 | |
aa4fb64e AE |
1927 | /** |
1928 | * Executes provided callback if scroll threshold is reached. Usuable to determine | |
1929 | * if user reached the bottom of an element to load new elements on the fly. | |
1930 | * | |
1931 | * If you do not provide a value for 'reference' and 'target' it will assume you're | |
1932 | * monitoring page scrolls, otherwise a valid jQuery selector must be provided for both. | |
1933 | * | |
1934 | * @param integer threshold | |
1935 | * @param object callback | |
1936 | * @param string reference | |
1937 | * @param string target | |
1938 | */ | |
1939 | WCF.Action.Scroll = Class.extend({ | |
1940 | /** | |
1941 | * callback used once threshold is reached | |
1942 | * @var object | |
1943 | */ | |
1944 | _callback: null, | |
1945 | ||
1946 | /** | |
1947 | * reference object | |
1948 | * @var jQuery | |
1949 | */ | |
1950 | _reference: null, | |
1951 | ||
1952 | /** | |
1953 | * target object | |
1954 | * @var jQuery | |
1955 | */ | |
1956 | _target: null, | |
1957 | ||
1958 | /** | |
1959 | * threshold value | |
1960 | * @var integer | |
1961 | */ | |
1962 | _threshold: 0, | |
1963 | ||
1964 | /** | |
1965 | * Initializes a new WCF.Action.Scroll object. | |
1966 | * | |
1967 | * @param integer threshold | |
1968 | * @param object callback | |
1969 | * @param string reference | |
1970 | * @param string target | |
1971 | */ | |
1972 | init: function(threshold, callback, reference, target) { | |
1973 | this._threshold = parseInt(threshold); | |
1974 | if (this._threshold === 0) { | |
1975 | console.debug("[WCF.Action.Scroll] Given threshold is invalid, aborting."); | |
1976 | return; | |
1977 | } | |
1978 | ||
1979 | if ($.isFunction(callback)) this._callback = callback; | |
1980 | if (this._callback === null) { | |
1981 | console.debug("[WCF.Action.Scroll] Given callback is invalid, aborting."); | |
1982 | return; | |
1983 | } | |
1984 | ||
1985 | // bind element references | |
1986 | this._reference = $((reference) ? reference : window); | |
1987 | this._target = $((target) ? target : document); | |
1988 | ||
1989 | // watch for scroll event | |
1990 | this.start(); | |
1991 | }, | |
1992 | ||
1993 | /** | |
1994 | * Calculates if threshold is reached and notifies callback. | |
1995 | */ | |
1996 | _scroll: function() { | |
1997 | var $targetHeight = this._target.height(); | |
1998 | var $topOffset = this._reference.scrollTop(); | |
1999 | var $referenceHeight = this._reference.height(); | |
2000 | ||
2001 | // calculate if defined threshold is visible | |
2002 | if (($targetHeight - ($referenceHeight + $topOffset)) < this._threshold) { | |
2003 | this._callback(this); | |
2004 | } | |
2005 | }, | |
2006 | ||
2007 | /** | |
2008 | * Enables scroll monitoring, may be used to resume. | |
2009 | */ | |
2010 | start: function() { | |
2011 | this._reference.on('scroll', $.proxy(this._scroll, this)); | |
221fce41 | 2012 | }, |
aa4fb64e AE |
2013 | |
2014 | /** | |
2015 | * Disables scroll monitoring, e.g. no more elements loadable. | |
2016 | */ | |
2017 | stop: function() { | |
2018 | this._reference.off('scroll'); | |
2019 | } | |
2020 | }); | |
2021 | ||
158bd3ca TD |
2022 | /** |
2023 | * Namespace for date-related functions. | |
2024 | */ | |
2025 | WCF.Date = {}; | |
2026 | ||
81f55d8f AE |
2027 | /** |
2028 | * Provides a date picker for date input fields. | |
2029 | */ | |
2030 | WCF.Date.Picker = { | |
2031 | /** | |
2032 | * Initializes the jQuery UI based date picker. | |
2033 | */ | |
2034 | init: function() { | |
c5286020 AE |
2035 | this._initDatePicker(); |
2036 | ||
2037 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Date.Picker', $.proxy(this._initDatePicker, this)); | |
2038 | }, | |
2039 | ||
2040 | /** | |
2041 | * Initializes the date picker for valid fields. | |
2042 | */ | |
2043 | _initDatePicker: function() { | |
2044 | $('input[type=date]:not(.jsDatePicker)').each(function(index, input) { | |
81f55d8f | 2045 | // do *not* use .attr() |
c5286020 | 2046 | var $input = $(input).prop('type', 'text').addClass('jsDatePicker'); |
81f55d8f AE |
2047 | |
2048 | // TODO: we should support all these braindead date formats, at least within output | |
2049 | $input.datepicker({ | |
2050 | changeMonth: true, | |
2051 | changeYear: true, | |
7b658f88 | 2052 | showOtherMonths: true, |
81f55d8f AE |
2053 | dateFormat: 'yy-mm-dd', |
2054 | yearRange: '1900:2038' // TODO: make it configurable? | |
2055 | }); | |
2056 | }); | |
2057 | } | |
2058 | }; | |
2059 | ||
158bd3ca TD |
2060 | /** |
2061 | * Provides utility functions for date operations. | |
2062 | */ | |
2063 | WCF.Date.Util = { | |
2064 | /** | |
2065 | * Returns UTC timestamp, if date is not given, current time will be used. | |
2066 | * | |
2067 | * @param Date date | |
2068 | * @return integer | |
2069 | */ | |
2070 | gmdate: function(date) { | |
2071 | var $date = (date) ? date : new Date(); | |
2072 | ||
2073 | return Math.round(Date.UTC( | |
2074 | $date.getUTCFullYear(), | |
2075 | $date.getUTCMonth(), | |
2076 | $date.getUTCDay(), | |
2077 | $date.getUTCHours(), | |
2078 | $date.getUTCMinutes(), | |
2079 | $date.getUTCSeconds() | |
2080 | ) / 1000); | |
2081 | }, | |
2082 | ||
2083 | /** | |
2084 | * Returns a Date object with precise offset (including timezone and local timezone). | |
2085 | * Parameter timestamp must be in miliseconds! | |
2086 | * | |
2087 | * @param integer timestamp | |
2088 | * @param integer offset | |
2089 | * @return Date | |
2090 | */ | |
2091 | getTimezoneDate: function(timestamp, offset) { | |
2092 | var $date = new Date(timestamp); | |
2093 | var $localOffset = $date.getTimezoneOffset() * -1 * 60000; | |
2094 | ||
2095 | return new Date((timestamp - $localOffset - offset)); | |
2096 | } | |
2097 | }; | |
2098 | ||
2099 | /** | |
2100 | * Handles relative time designations. | |
2101 | */ | |
81f55d8f | 2102 | WCF.Date.Time = Class.extend({ |
158bd3ca TD |
2103 | /** |
2104 | * Initializes relative datetimes. | |
2105 | */ | |
2106 | init: function() { | |
2107 | // initialize variables | |
2108 | this.elements = $('time.datetime'); | |
2109 | this.timestamp = 0; | |
2110 | ||
2111 | // calculate relative datetime on init | |
2112 | this._refresh(); | |
2113 | ||
2114 | // re-calculate relative datetime every minute | |
2115 | new WCF.PeriodicalExecuter($.proxy(this._refresh, this), 60000); | |
9f959ced | 2116 | |
9c1e5045 AE |
2117 | // bind dom node inserted listener |
2118 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Date.Time', $.proxy(this._domNodeInserted, this)); | |
2119 | }, | |
9f959ced | 2120 | |
9c1e5045 AE |
2121 | /** |
2122 | * Updates element collection once a DOM node was inserted. | |
2123 | */ | |
2124 | _domNodeInserted: function() { | |
2125 | this.elements = $('time.datetime'); | |
2126 | this._refresh(); | |
158bd3ca TD |
2127 | }, |
2128 | ||
2129 | /** | |
2130 | * Refreshes relative datetime for each element. | |
2131 | */ | |
2132 | _refresh: function() { | |
2133 | // TESTING ONLY! | |
2134 | var $date = new Date(); | |
2135 | this.timestamp = ($date.getTime() - $date.getMilliseconds()) / 1000; | |
2136 | // TESTING ONLY! | |
2137 | ||
2138 | this.elements.each($.proxy(this._refreshElement, this)); | |
2139 | }, | |
2140 | ||
2141 | /** | |
2142 | * Refreshes relative datetime for current element. | |
2143 | * | |
2144 | * @param integer index | |
2145 | * @param object element | |
2146 | */ | |
2147 | _refreshElement: function(index, element) { | |
2148 | if (!$(element).attr('title')) { | |
2149 | $(element).attr('title', $(element).text()); | |
2150 | } | |
2151 | ||
2152 | var $timestamp = $(element).data('timestamp'); | |
2153 | var $date = $(element).data('date'); | |
2154 | var $time = $(element).data('time'); | |
2155 | var $offset = $(element).data('offset'); | |
2156 | ||
f3114f09 TD |
2157 | // timestamp is in the future |
2158 | if ($timestamp > this.timestamp) { | |
2159 | var $string = WCF.Language.get('wcf.date.dateTimeFormat'); | |
2160 | $(element).text($string.replace(/\%date\%/, $date).replace(/\%time\%/, $time)); | |
2161 | } | |
158bd3ca | 2162 | // timestamp is less than 60 minutes ago (display 1 hour ago rather than 60 minutes ago) |
f3114f09 | 2163 | else if (this.timestamp < ($timestamp + 3540)) { |
158bd3ca | 2164 | var $minutes = Math.round((this.timestamp - $timestamp) / 60); |
e89c399a | 2165 | $(element).text(eval(WCF.Language.get('wcf.date.relative.minutes'))); |
158bd3ca TD |
2166 | } |
2167 | // timestamp is less than 24 hours ago | |
2168 | else if (this.timestamp < ($timestamp + 86400)) { | |
2169 | var $hours = Math.round((this.timestamp - $timestamp) / 3600); | |
e89c399a | 2170 | $(element).text(eval(WCF.Language.get('wcf.date.relative.hours'))); |
158bd3ca TD |
2171 | } |
2172 | // timestamp is less than a week ago | |
2173 | else if (this.timestamp < ($timestamp + 604800)) { | |
2174 | var $days = Math.round((this.timestamp - $timestamp) / 86400); | |
e89c399a | 2175 | var $string = eval(WCF.Language.get('wcf.date.relative.pastDays')); |
9f959ced | 2176 | |
158bd3ca TD |
2177 | // get day of week |
2178 | var $dateObj = WCF.Date.Util.getTimezoneDate(($timestamp * 1000), $offset); | |
2179 | var $dow = $dateObj.getDay(); | |
2180 | ||
2181 | $(element).text($string.replace(/\%day\%/, WCF.Language.get('__days')[$dow]).replace(/\%time\%/, $time)); | |
2182 | } | |
2183 | // timestamp is between ~700 million years BC and last week | |
2184 | else { | |
e89c399a | 2185 | var $string = WCF.Language.get('wcf.date.dateTimeFormat'); |
158bd3ca TD |
2186 | $(element).text($string.replace(/\%date\%/, $date).replace(/\%time\%/, $time)); |
2187 | } | |
2188 | } | |
81f55d8f | 2189 | }); |
158bd3ca TD |
2190 | |
2191 | /** | |
2192 | * Hash-like dictionary. Based upon idead from Prototype's hash | |
2193 | * | |
2194 | * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/hash.js | |
2195 | */ | |
39e27190 | 2196 | WCF.Dictionary = Class.extend({ |
264e3f79 AE |
2197 | /** |
2198 | * list of variables | |
2199 | * @var object | |
2200 | */ | |
2201 | _variables: { }, | |
2202 | ||
158bd3ca TD |
2203 | /** |
2204 | * Initializes a new dictionary. | |
2205 | */ | |
83428b7f AE |
2206 | init: function() { |
2207 | this._variables = { }; | |
2208 | }, | |
158bd3ca TD |
2209 | |
2210 | /** | |
2211 | * Adds an entry. | |
2212 | * | |
2213 | * @param string key | |
2214 | * @param mixed value | |
2215 | */ | |
2216 | add: function(key, value) { | |
264e3f79 | 2217 | this._variables[key] = value; |
158bd3ca TD |
2218 | }, |
2219 | ||
2220 | /** | |
2221 | * Adds a traditional object to current dataset. | |
2222 | * | |
2223 | * @param object object | |
2224 | */ | |
2225 | addObject: function(object) { | |
2226 | for (var $key in object) { | |
2227 | this.add($key, object[$key]); | |
2228 | } | |
2229 | }, | |
2230 | ||
2231 | /** | |
2232 | * Adds a dictionary to current dataset. | |
2233 | * | |
2234 | * @param object dictionary | |
2235 | */ | |
2236 | addDictionary: function(dictionary) { | |
2237 | dictionary.each($.proxy(function(pair) { | |
2238 | this.add(pair.key, pair.value); | |
2239 | }, this)); | |
2240 | }, | |
2241 | ||
2242 | /** | |
2243 | * Retrieves the value of an entry or returns null if key is not found. | |
2244 | * | |
2245 | * @param string key | |
2246 | * @returns mixed | |
2247 | */ | |
2248 | get: function(key) { | |
2249 | if (this.isset(key)) { | |
264e3f79 | 2250 | return this._variables[key]; |
158bd3ca TD |
2251 | } |
2252 | ||
2253 | return null; | |
2254 | }, | |
2255 | ||
2256 | /** | |
2257 | * Returns true if given key is a valid entry. | |
2258 | * | |
2259 | * @param string key | |
2260 | */ | |
2261 | isset: function(key) { | |
264e3f79 | 2262 | return this._variables.hasOwnProperty(key); |
158bd3ca TD |
2263 | }, |
2264 | ||
2265 | /** | |
2266 | * Removes an entry. | |
2267 | * | |
2268 | * @param string key | |
2269 | */ | |
2270 | remove: function(key) { | |
264e3f79 | 2271 | delete this._variables[key]; |
158bd3ca TD |
2272 | }, |
2273 | ||
2274 | /** | |
2275 | * Iterates through dictionary. | |
2276 | * | |
2277 | * Usage: | |
2278 | * var $hash = new WCF.Dictionary(); | |
2279 | * $hash.add('foo', 'bar'); | |
2280 | * $hash.each(function(pair) { | |
2281 | * // alerts: foo = bar | |
2282 | * alert(pair.key + ' = ' + pair.value); | |
2283 | * }); | |
2284 | * | |
2285 | * @param function callback | |
2286 | */ | |
2287 | each: function(callback) { | |
2288 | if (!$.isFunction(callback)) { | |
2289 | return; | |
2290 | } | |
2291 | ||
264e3f79 AE |
2292 | for (var $key in this._variables) { |
2293 | var $value = this._variables[$key]; | |
158bd3ca TD |
2294 | var $pair = { |
2295 | key: $key, | |
2296 | value: $value | |
2297 | }; | |
2298 | ||
2299 | callback($pair); | |
2300 | } | |
264e3f79 AE |
2301 | }, |
2302 | ||
2303 | /** | |
2304 | * Returns the amount of items. | |
2305 | * | |
2306 | * @return integer | |
2307 | */ | |
2308 | count: function() { | |
2309 | return $.getLength(this._variables); | |
2310 | }, | |
2311 | ||
2312 | /** | |
2313 | * Returns true, if dictionary is empty. | |
2314 | * | |
2315 | * @return integer | |
2316 | */ | |
2317 | isEmpty: function() { | |
2318 | return !this.count(); | |
158bd3ca | 2319 | } |
39e27190 | 2320 | }); |
158bd3ca TD |
2321 | |
2322 | /** | |
2323 | * Global language storage. | |
2324 | * | |
2325 | * @see WCF.Dictionary | |
2326 | */ | |
2327 | WCF.Language = { | |
2328 | _variables: new WCF.Dictionary(), | |
2329 | ||
2330 | /** | |
f2e420cc | 2331 | * @see WCF.Dictionary.add() |
158bd3ca TD |
2332 | */ |
2333 | add: function(key, value) { | |
2334 | this._variables.add(key, value); | |
2335 | }, | |
2336 | ||
2337 | /** | |
2338 | * @see WCF.Dictionary.addObject() | |
2339 | */ | |
2340 | addObject: function(object) { | |
2341 | this._variables.addObject(object); | |
2342 | }, | |
2343 | ||
2344 | /** | |
2345 | * Retrieves a variable. | |
2346 | * | |
2347 | * @param string key | |
2348 | * @return mixed | |
2349 | */ | |
12876b18 TD |
2350 | get: function(key, parameters) { |
2351 | // initialize parameters with an empty object | |
2352 | if (typeof parameters === 'undefined') var parameters = {}; | |
2353 | ||
2354 | var value = this._variables.get(key); | |
2355 | ||
2356 | if (typeof value === 'string') { | |
2357 | // transform strings into template and try to refetch | |
2358 | this.add(key, new WCF.Template(value)); | |
2359 | return this.get(key, parameters); | |
2360 | } | |
2361 | else if (value !== null && typeof value === 'object' && typeof value.fetch !== 'undefined') { | |
2362 | // evaluate templates | |
2363 | value = value.fetch(parameters); | |
2364 | } | |
9a61f9d7 TD |
2365 | else if (value === null) { |
2366 | // return key again | |
2367 | return key; | |
2368 | } | |
12876b18 TD |
2369 | |
2370 | return value; | |
158bd3ca TD |
2371 | } |
2372 | }; | |
f2e420cc | 2373 | |
2b4d2743 AE |
2374 | /** |
2375 | * Handles multiple language input fields. | |
2376 | * | |
2377 | * @param string elementID | |
2378 | * @param boolean forceSelection | |
2379 | * @param object values | |
2380 | * @param object availableLanguages | |
2381 | */ | |
7382b20f | 2382 | WCF.MultipleLanguageInput = Class.extend({ |
2b4d2743 AE |
2383 | /** |
2384 | * list of available languages | |
2385 | * @var object | |
2386 | */ | |
2387 | _availableLanguages: {}, | |
9f959ced | 2388 | |
2b4d2743 AE |
2389 | /** |
2390 | * initialization state | |
2391 | * @var boolean | |
2392 | */ | |
2393 | _didInit: false, | |
9f959ced | 2394 | |
2b4d2743 AE |
2395 | /** |
2396 | * target input element | |
2397 | * @var jQuery | |
2398 | */ | |
2399 | _element: null, | |
38047181 AE |
2400 | |
2401 | /** | |
2402 | * true, if data was entered after initialization | |
2403 | * @var boolean | |
2404 | */ | |
2405 | _insertedDataAfterInit: false, | |
9f959ced | 2406 | |
2b4d2743 AE |
2407 | /** |
2408 | * enables multiple language ability | |
2409 | * @var boolean | |
2410 | */ | |
2411 | _isEnabled: false, | |
9f959ced | 2412 | |
2b4d2743 AE |
2413 | /** |
2414 | * enforce multiple language ability | |
2415 | * @var boolean | |
2416 | */ | |
2417 | _forceSelection: false, | |
9f959ced | 2418 | |
2b4d2743 AE |
2419 | /** |
2420 | * currently active language id | |
2421 | * @var integer | |
2422 | */ | |
2423 | _languageID: 0, | |
9f959ced | 2424 | |
2b4d2743 AE |
2425 | /** |
2426 | * language selection list | |
2427 | * @var jQuery | |
2428 | */ | |
2429 | _list: null, | |
9f959ced | 2430 | |
2b4d2743 AE |
2431 | /** |
2432 | * list of language values on init | |
2433 | * @var object | |
2434 | */ | |
2435 | _values: null, | |
9f959ced | 2436 | |
2b4d2743 AE |
2437 | /** |
2438 | * Initializes multiple language ability for given element id. | |
2439 | * | |
2440 | * @param integer elementID | |
2441 | * @param boolean forceSelection | |
2442 | * @param boolean isEnabled | |
2443 | * @param object values | |
2444 | * @param object availableLanguages | |
2445 | */ | |
2446 | init: function(elementID, forceSelection, values, availableLanguages) { | |
2447 | this._element = $('#' + $.wcfEscapeID(elementID)); | |
2448 | this._forceSelection = forceSelection; | |
2449 | this._values = values; | |
2450 | this._availableLanguages = availableLanguages; | |
2451 | ||
2452 | // default to current user language | |
2453 | this._languageID = LANGUAGE_ID; | |
2454 | if (this._element.length == 0) { | |
2455 | console.debug("[WCF.MultipleLanguageInput] element id '" + elementID + "' is unknown"); | |
2456 | return; | |
2457 | } | |
2458 | ||
2459 | // build selection handler | |
2460 | var $enableOnInit = ($.getLength(this._values) > 0) ? true : false; | |
38047181 | 2461 | this._insertedDataAfterInit = $enableOnInit; |
2b4d2743 AE |
2462 | this._prepareElement($enableOnInit); |
2463 | ||
2464 | // listen for submit event | |
2465 | this._element.parents('form').submit($.proxy(this._submit, this)); | |
9f959ced | 2466 | |
2b4d2743 AE |
2467 | this._didInit = true; |
2468 | }, | |
9f959ced | 2469 | |
2b4d2743 AE |
2470 | /** |
2471 | * Builds language handler. | |
2472 | * | |
2473 | * @param boolean enableOnInit | |
2474 | */ | |
2475 | _prepareElement: function(enableOnInit) { | |
b85dd964 MS |
2476 | // enable DOMNodeInserted event |
2477 | WCF.DOMNodeInsertedHandler.enable(); | |
2478 | ||
184a8d6d | 2479 | this._element.wrap('<div class="dropdown preInput" />'); |
2b4d2743 | 2480 | var $wrapper = this._element.parent(); |
184a8d6d | 2481 | var $button = $('<p class="button dropdownToggle"><span>' + WCF.Language.get('wcf.global.button.disabledI18n') + '</span></p>').prependTo($wrapper); |
0d819c04 | 2482 | $button.data('target', $wrapper.wcfIdentify()).click($.proxy(this._enable, this)); |
184a8d6d | 2483 | |
7382b20f AE |
2484 | // insert list |
2485 | this._list = $('<ul class="dropdownMenu"></ul>').insertAfter($button); | |
2486 | ||
184a8d6d | 2487 | // add a special class if next item is a textarea |
7382b20f | 2488 | if ($button.nextAll('textarea').length) { |
184a8d6d AE |
2489 | $button.addClass('dropdownCaptionTextarea'); |
2490 | } | |
2491 | else { | |
2492 | $button.addClass('dropdownCaption'); | |
1b049030 | 2493 | } |
184a8d6d AE |
2494 | |
2495 | // insert available languages | |
2496 | for (var $languageID in this._availableLanguages) { | |
2497 | $('<li><span>' + this._availableLanguages[$languageID] + '</span></li>').data('languageID', $languageID).click($.proxy(this._changeLanguage, this)).appendTo(this._list); | |
2498 | } | |
9f959ced | 2499 | |
184a8d6d AE |
2500 | // disable language input |
2501 | if (!this._forceSelection) { | |
2502 | $('<li class="dropdownDivider" />').appendTo(this._list); | |
2503 | $('<li><span>' + WCF.Language.get('wcf.global.button.disabledI18n') + '</span></li>').click($.proxy(this._disable, this)).appendTo(this._list); | |
2504 | } | |
2b4d2743 | 2505 | |
46b44739 | 2506 | if (enableOnInit || this._forceSelection) { |
0eddcb5c | 2507 | $button.trigger('click'); |
9f959ced | 2508 | |
2b4d2743 AE |
2509 | // pre-select current language |
2510 | this._list.children('li').each($.proxy(function(index, listItem) { | |
2511 | var $listItem = $(listItem); | |
2512 | if ($listItem.data('languageID') == this._languageID) { | |
2513 | $listItem.trigger('click'); | |
2514 | } | |
2515 | }, this)); | |
2516 | } | |
184a8d6d AE |
2517 | |
2518 | WCF.Dropdown.registerCallback($wrapper.wcfIdentify(), $.proxy(this._handleAction, this)); | |
b85dd964 | 2519 | |
5945e559 | 2520 | // disable DOMNodeInserted event |
b85dd964 | 2521 | WCF.DOMNodeInsertedHandler.disable(); |
184a8d6d AE |
2522 | }, |
2523 | ||
2524 | /** | |
2525 | * Handles dropdown actions. | |
2526 | * | |
2527 | * @param jQuery dropdown | |
2528 | * @param string action | |
2529 | */ | |
2530 | _handleAction: function(dropdown, action) { | |
2531 | if (action === 'close') { | |
2532 | this._closeSelection(); | |
2533 | } | |
2b4d2743 | 2534 | }, |
9f959ced | 2535 | |
2b4d2743 AE |
2536 | /** |
2537 | * Enables the language selection or shows the selection if already enabled. | |
2538 | * | |
2539 | * @param object event | |
2540 | */ | |
2541 | _enable: function(event) { | |
2542 | if (!this._isEnabled) { | |
184a8d6d | 2543 | var $button = $(event.currentTarget); |
0d819c04 AE |
2544 | $button.next('.dropdownMenu').css({ |
2545 | top: ($button.outerHeight() - 1) + 'px' | |
2546 | }); | |
7382b20f | 2547 | |
184a8d6d AE |
2548 | if ($button.getTagName() === 'p') { |
2549 | $button = $button.children('span:eq(0)'); | |
0eddcb5c | 2550 | } |
184a8d6d | 2551 | |
2b4d2743 | 2552 | $button.addClass('active'); |
184a8d6d | 2553 | |
2b4d2743 AE |
2554 | this._isEnabled = true; |
2555 | } | |
184a8d6d | 2556 | |
2b4d2743 AE |
2557 | // toggle list |
2558 | if (this._list.is(':visible')) { | |
2b4d2743 AE |
2559 | this._showSelection(); |
2560 | } | |
9f959ced | 2561 | |
2b4d2743 AE |
2562 | // discard event |
2563 | event.stopPropagation(); | |
2564 | }, | |
9f959ced | 2565 | |
2b4d2743 AE |
2566 | /** |
2567 | * Shows the language selection. | |
2568 | */ | |
2569 | _showSelection: function() { | |
2570 | if (this._isEnabled) { | |
2571 | // display status for each language | |
2572 | this._list.children('li').each($.proxy(function(index, listItem) { | |
2573 | var $listItem = $(listItem); | |
2574 | var $languageID = $listItem.data('languageID'); | |
9f959ced | 2575 | |
2b4d2743 AE |
2576 | if ($languageID) { |
2577 | if (this._values[$languageID] && this._values[$languageID] != '') { | |
2578 | $listItem.removeClass('missingValue'); | |
2579 | } | |
2580 | else { | |
2581 | $listItem.addClass('missingValue'); | |
2582 | } | |
2583 | } | |
2584 | }, this)); | |
2b4d2743 AE |
2585 | } |
2586 | }, | |
9f959ced | 2587 | |
2b4d2743 AE |
2588 | /** |
2589 | * Closes the language selection. | |
2590 | */ | |
2591 | _closeSelection: function() { | |
055c009e | 2592 | this._disable(); |
2b4d2743 | 2593 | }, |
9f959ced | 2594 | |
2b4d2743 AE |
2595 | /** |
2596 | * Changes the currently active language. | |
2597 | * | |
2598 | * @param object event | |
2599 | */ | |
2600 | _changeLanguage: function(event) { | |
184a8d6d | 2601 | var $button = $(event.currentTarget); |
38047181 AE |
2602 | this._insertedDataAfterInit = true; |
2603 | ||
2b4d2743 AE |
2604 | // save current value |
2605 | if (this._didInit) { | |
2606 | this._values[this._languageID] = this._element.val(); | |
2607 | } | |
38047181 | 2608 | |
2b4d2743 AE |
2609 | // set new language |
2610 | this._languageID = $button.data('languageID'); | |
2611 | if (this._values[this._languageID]) { | |
2612 | this._element.val(this._values[this._languageID]); | |
2613 | } | |
2614 | else { | |
2615 | this._element.val(''); | |
2616 | } | |
38047181 | 2617 | |
2b4d2743 AE |
2618 | // update marking |
2619 | this._list.children('li').removeClass('active'); | |
2620 | $button.addClass('active'); | |
38047181 | 2621 | |
2b4d2743 | 2622 | // update label |
fbdafcc9 | 2623 | this._list.prev('.dropdownToggle').children('span').text(this._availableLanguages[this._languageID]); |
38047181 | 2624 | |
2b4d2743 | 2625 | // close selection and set focus on input element |
055c009e | 2626 | //this._closeSelection(); |
84481c65 | 2627 | this._element.blur().focus(); |
2b4d2743 | 2628 | }, |
9f959ced | 2629 | |
2b4d2743 AE |
2630 | /** |
2631 | * Disables language selection for current element. | |
0bd3dc4b AE |
2632 | * |
2633 | * @param object event | |
2b4d2743 | 2634 | */ |
0bd3dc4b | 2635 | _disable: function(event) { |
055c009e AE |
2636 | if (event === undefined && this._insertedDataAfterInit) { |
2637 | event = null; | |
2638 | } | |
2639 | ||
2640 | if (this._forceSelection || !this._list || event === null) { | |
46b44739 AE |
2641 | return; |
2642 | } | |
2643 | ||
2b4d2743 | 2644 | // remove active marking |
fbdafcc9 | 2645 | this._list.prev('.dropdownToggle').children('span').removeClass('active').text(WCF.Language.get('wcf.global.button.disabledI18n')); |
9f959ced | 2646 | |
2b4d2743 AE |
2647 | // update element value |
2648 | if (this._values[LANGUAGE_ID]) { | |
2649 | this._element.val(this._values[LANGUAGE_ID]); | |
2650 | } | |
2651 | else { | |
2652 | // no value for current language found, proceed with empty input | |
2653 | this._element.val(); | |
2654 | } | |
84481c65 AE |
2655 | |
2656 | this._element.blur(); | |
055c009e | 2657 | this._insertedDataAfterInit = false; |
2b4d2743 | 2658 | this._isEnabled = false; |
055c009e | 2659 | this._values = { }; |
2b4d2743 | 2660 | }, |
9f959ced | 2661 | |
2b4d2743 AE |
2662 | /** |
2663 | * Prepares language variables on before submit. | |
2664 | */ | |
2665 | _submit: function() { | |
2666 | // insert hidden form elements on before submit | |
2667 | if (!this._isEnabled) { | |
055c009e | 2668 | return 0xDEADBEEF; |
2b4d2743 | 2669 | } |
9f959ced | 2670 | |
2b4d2743 AE |
2671 | // fetch active value |
2672 | if (this._languageID) { | |
2673 | this._values[this._languageID] = this._element.val(); | |
2674 | } | |
9f959ced | 2675 | |
2b4d2743 AE |
2676 | var $form = $(this._element.parents('form')[0]); |
2677 | var $elementID = this._element.wcfIdentify(); | |
9f959ced | 2678 | |
2b4d2743 AE |
2679 | for (var $languageID in this._values) { |
2680 | $('<input type="hidden" name="' + $elementID + '_i18n[' + $languageID + ']" value="' + this._values[$languageID] + '" />').appendTo($form); | |
2681 | } | |
9f959ced | 2682 | |
2b4d2743 AE |
2683 | // remove name attribute to prevent conflict with i18n values |
2684 | this._element.removeAttr('name'); | |
2685 | } | |
7382b20f | 2686 | }); |
2b4d2743 | 2687 | |
f2e420cc AE |
2688 | /** |
2689 | * Icon collection used across all JavaScript classes. | |
2690 | * | |
2691 | * @see WCF.Dictionary | |
2692 | */ | |
2693 | WCF.Icon = { | |
2694 | /** | |
2695 | * list of icons | |
2696 | * @var WCF.Dictionary | |
2697 | */ | |
2698 | _icons: new WCF.Dictionary(), | |
9f959ced | 2699 | |
f2e420cc AE |
2700 | /** |
2701 | * @see WCF.Dictionary.add() | |
2702 | */ | |
2703 | add: function(name, path) { | |
2704 | this._icons.add(name, path); | |
2705 | }, | |
9f959ced | 2706 | |
f2e420cc AE |
2707 | /** |
2708 | * @see WCF.Dictionary.addObject() | |
2709 | */ | |
2710 | addObject: function(object) { | |
2711 | this._icons.addObject(object); | |
2712 | }, | |
9f959ced | 2713 | |
f2e420cc AE |
2714 | /** |
2715 | * @see WCF.Dictionary.get() | |
2716 | */ | |
2717 | get: function(name) { | |
2718 | return this._icons.get(name); | |
2719 | } | |
2720 | }; | |
158bd3ca | 2721 | |
52560d9e TD |
2722 | /** |
2723 | * Number utilities. | |
2724 | */ | |
2725 | WCF.Number = { | |
2726 | /** | |
2727 | * Rounds a number to a given number of floating points digits. Defaults to 0. | |
2728 | * | |
2729 | * @param number number | |
2730 | * @param floatingPoint number of digits | |
2731 | * @return number | |
2732 | */ | |
2733 | round: function (number, floatingPoint) { | |
2734 | floatingPoint = Math.pow(10, (floatingPoint || 0)); | |
2735 | ||
2736 | return Math.round(number * floatingPoint) / floatingPoint; | |
2737 | } | |
f4126129 | 2738 | }; |
52560d9e | 2739 | |
158bd3ca TD |
2740 | /** |
2741 | * String utilities. | |
2742 | */ | |
2743 | WCF.String = { | |
52560d9e TD |
2744 | /** |
2745 | * Adds thousands separators to a given number. | |
2746 | * | |
2747 | * @param mixed number | |
2748 | * @return string | |
2749 | */ | |
2750 | addThousandsSeparator: function(number) { | |
2751 | var $numberString = String(number); | |
24c42d67 | 2752 | var parts = $numberString.split(/[^0-9]+/); |
9f959ced | 2753 | |
24c42d67 | 2754 | var $decimalPoint = $numberString.match(/[^0-9]+/); |
52560d9e TD |
2755 | |
2756 | $numberString = parts[0]; | |
f4126129 TD |
2757 | var $decimalPart = ''; |
2758 | if ($decimalPoint !== null) { | |
24c42d67 TD |
2759 | delete parts[0]; |
2760 | var $decimalPart = $decimalPoint.join('')+parts.join(''); | |
2761 | } | |
52560d9e TD |
2762 | if (parseInt(number) >= 1000 || parseInt(number) <= -1000) { |
2763 | var $negative = false; | |
2764 | if (parseInt(number) <= -1000) { | |
2765 | $negative = true; | |
2766 | $numberString = $numberString.substring(1); | |
2767 | } | |
2768 | var $separator = WCF.Language.get('wcf.global.thousandsSeparator'); | |
2769 | ||
2770 | if ($separator != null && $separator != '') { | |
2771 | var $numElements = new Array(); | |
f4126129 | 2772 | var $firstPart = $numberString.length % 3; |
52560d9e TD |
2773 | if ($firstPart == 0) $firstPart = 3; |
2774 | for (var $i = 0; $i < Math.ceil($numberString.length / 3); $i++) { | |
2775 | if ($i == 0) $numElements.push($numberString.substring(0, $firstPart)); | |
2776 | else { | |
f4126129 | 2777 | var $start = (($i - 1) * 3) + $firstPart; |
52560d9e TD |
2778 | $numElements.push($numberString.substring($start, $start + 3)); |
2779 | } | |
2780 | } | |
2781 | $numberString = (($negative) ? ('-') : ('')) + $numElements.join($separator); | |
2782 | } | |
2783 | } | |
2784 | ||
2785 | return $numberString + $decimalPart; | |
2786 | }, | |
2787 | ||
158bd3ca TD |
2788 | /** |
2789 | * Escapes special HTML-characters within a string | |
2790 | * | |
2791 | * @param string string | |
2792 | * @return string | |
2793 | */ | |
2794 | escapeHTML: function (string) { | |
2795 | return string.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); | |
2796 | }, | |
2797 | ||
2798 | /** | |
2799 | * Escapes a String to work with RegExp. | |
2800 | * | |
2801 | * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25 | |
2802 | * @param string string | |
2803 | * @return string | |
2804 | */ | |
2805 | escapeRegExp: function(string) { | |
2806 | return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); | |
2807 | }, | |
2808 | ||
2809 | /** | |
52560d9e | 2810 | * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands-separators |
158bd3ca | 2811 | * |
52560d9e | 2812 | * @param mixed number |
158bd3ca TD |
2813 | * @return string |
2814 | */ | |
52560d9e TD |
2815 | formatNumeric: function(number, floatingPoint) { |
2816 | number = String(WCF.Number.round(number, floatingPoint || 2)); | |
2817 | number = number.replace('.', WCF.Language.get('wcf.global.decimalPoint')); | |
2818 | ||
2819 | return this.addThousandsSeparator(number); | |
158bd3ca TD |
2820 | }, |
2821 | ||
7a17b105 MS |
2822 | /** |
2823 | * Makes a string's first character lowercase | |
2824 | * | |
2825 | * @param string string | |
2826 | * @return string | |
2827 | */ | |
2828 | lcfirst: function(string) { | |
2829 | return string.substring(0, 1).toLowerCase() + string.substring(1); | |
2830 | }, | |
2831 | ||
158bd3ca | 2832 | /** |
52560d9e | 2833 | * Makes a string's first character uppercase |
158bd3ca | 2834 | * |
52560d9e | 2835 | * @param string string |
158bd3ca TD |
2836 | * @return string |
2837 | */ | |
52560d9e TD |
2838 | ucfirst: function(string) { |
2839 | return string.substring(0, 1).toUpperCase() + string.substring(1); | |
158bd3ca TD |
2840 | } |
2841 | }; | |
2842 | ||
2843 | /** | |
2844 | * Basic implementation for WCF TabMenus. Use the data attributes 'active' to specify the | |
2845 | * tab which should be shown on init. Furthermore you may specify a 'store' data-attribute | |
2846 | * which will be filled with the currently selected tab. | |
2847 | */ | |
2848 | WCF.TabMenu = { | |
dbd319de AE |
2849 | /** |
2850 | * list of tabmenu containers | |
2851 | * @var object | |
2852 | */ | |
2853 | _containers: { }, | |
2854 | ||
f24f0823 AE |
2855 | /** |
2856 | * initialization state | |
2857 | * @var boolean | |
2858 | */ | |
dbd319de | 2859 | _didInit: false, |
f24f0823 | 2860 | |
158bd3ca TD |
2861 | /** |
2862 | * Initializes all TabMenus | |
2863 | */ | |
2864 | init: function() { | |
184a8d6d | 2865 | var $containers = $('.tabMenuContainer'); |
dbd319de | 2866 | var self = this; |
f24f0823 AE |
2867 | $containers.each(function(index, tabMenu) { |
2868 | var $tabMenu = $(tabMenu); | |
dbd319de AE |
2869 | var $containerID = $tabMenu.wcfIdentify(); |
2870 | if (self._containers[$containerID]) { | |
2871 | // continue with next container | |
2872 | return true; | |
158bd3ca TD |
2873 | } |
2874 | ||
016198a8 AE |
2875 | if ($tabMenu.data('store') && !$('#' + $tabMenu.data('store')).length) { |
2876 | $('<input type="hidden" name="' + $tabMenu.data('store') + '" value="" id="' + $tabMenu.data('store') + '" />').appendTo($tabMenu.parents('form').find('.formSubmit')); | |
2877 | } | |
2878 | ||
158bd3ca | 2879 | // init jQuery UI TabMenu |
dbd319de | 2880 | self._containers[$containerID] = $tabMenu; |
f24f0823 | 2881 | $tabMenu.wcfTabs({ |
158bd3ca TD |
2882 | select: function(event, ui) { |
2883 | var $panel = $(ui.panel); | |
184a8d6d | 2884 | var $container = $panel.closest('.tabMenuContainer'); |
158bd3ca TD |
2885 | |
2886 | // store currently selected item | |
016198a8 AE |
2887 | var $tabMenu = $container; |
2888 | while (true) { | |
2889 | // do not trigger on init | |
2890 | if ($tabMenu.data('isParent') === undefined) { | |
2891 | break; | |
2892 | } | |
2893 | ||
2894 | if ($tabMenu.data('isParent')) { | |
2895 | if ($tabMenu.data('store')) { | |
2896 | $('#' + $tabMenu.data('store')).val($panel.attr('id')); | |
2897 | } | |
2898 | ||
2899 | break; | |
2900 | } | |
2901 | else { | |
2902 | $tabMenu = $tabMenu.data('parent'); | |
158bd3ca TD |
2903 | } |
2904 | } | |
f24f0823 AE |
2905 | |
2906 | // set panel id as location hash | |
dbd319de | 2907 | if (WCF.TabMenu._didInit) { |
f24f0823 AE |
2908 | location.hash = '#' + $panel.attr('id'); |
2909 | } | |
c8148c1a MS |
2910 | |
2911 | $container.trigger('tabsselect', event, ui); | |
158bd3ca TD |
2912 | } |
2913 | }); | |
2914 | ||
47e7e4d9 | 2915 | $tabMenu.data('isParent', ($tabMenu.children('.tabMenuContainer, .tabMenuContent').length > 0)).data('parent', false); |
4b1f0bb6 AE |
2916 | if (!$tabMenu.data('isParent')) { |
2917 | // check if we're a child element | |
2918 | if ($tabMenu.parent().hasClass('tabMenuContainer')) { | |
2919 | $tabMenu.data('parent', $tabMenu.parent()); | |
2920 | } | |
2921 | } | |
158bd3ca | 2922 | }); |
f24f0823 AE |
2923 | |
2924 | // try to resolve location hash | |
dbd319de AE |
2925 | if (!this._didInit) { |
2926 | this.selectTabs(); | |
2927 | $(window).bind('hashchange', $.proxy(this.selectTabs, this)); | |
da804832 AE |
2928 | |
2929 | if (!this._selectErroneousTab()) { | |
2930 | this._selectActiveTab(); | |
2931 | } | |
dbd319de AE |
2932 | } |
2933 | ||
da804832 AE |
2934 | this._didInit = true; |
2935 | }, | |
2936 | ||
2937 | /** | |
2938 | * Force display of first erroneous tab, returns true, if at | |
2939 | * least one tab contains an error. | |
2940 | * | |
2941 | * @return boolean | |
2942 | */ | |
2943 | _selectErroneousTab: function() { | |
4b1f0bb6 AE |
2944 | for (var $containerID in this._containers) { |
2945 | var $tabMenu = this._containers[$containerID]; | |
da804832 | 2946 | |
4b1f0bb6 AE |
2947 | if (!$tabMenu.data('isParent') && $tabMenu.find('.formError').length) { |
2948 | while (true) { | |
2949 | if ($tabMenu.data('parent') === false) { | |
2950 | break; | |
2951 | } | |
2952 | ||
2953 | $tabMenu = $tabMenu.data('parent').wcfTabs('select', $tabMenu.wcfIdentify()); | |
2954 | } | |
2955 | ||
da804832 | 2956 | return true; |
4b1f0bb6 AE |
2957 | } |
2958 | } | |
2959 | ||
da804832 AE |
2960 | return false; |
2961 | }, | |
2962 | ||
2963 | /** | |
2964 | * Selects the active tab menu item. | |
2965 | */ | |
2966 | _selectActiveTab: function() { | |
2967 | for (var $containerID in this._containers) { | |
2968 | var $tabMenu = this._containers[$containerID]; | |
2969 | if ($tabMenu.data('active')) { | |
2970 | var $index = $tabMenu.data('active'); | |
2971 | var $subIndex = null; | |
2972 | if (/-/.test($index)) { | |
2973 | var $tmp = $index.split('-'); | |
2974 | $index = $tmp[0]; | |
2975 | $subIndex = $tmp[1]; | |
2976 | } | |
2977 | ||
2978 | $tabMenu.find('.tabMenuContent').each(function(innerIndex, tabMenuItem) { | |
2979 | var $tabMenuItem = $(tabMenuItem); | |
2980 | if ($tabMenuItem.wcfIdentify() == $index) { | |
2981 | $tabMenu.wcfTabs('select', innerIndex); | |
2982 | ||
2983 | if ($subIndex !== null) { | |
2984 | if ($tabMenuItem.hasClass('tabMenuContainer')) { | |
2985 | $tabMenuItem.wcfTabs('select', $tabMenu.data('active')); | |
2986 | } | |
2987 | else { | |
2988 | $tabMenu.wcfTabs('select', $tabMenu.data('active')); | |
2989 | } | |
2990 | } | |
2991 | ||
2992 | return false; | |
2993 | } | |
2994 | }); | |
2995 | } | |
2996 | } | |
dbd319de AE |
2997 | }, |
2998 | ||
2999 | /** | |
3000 | * Resolves location hash to display tab menus. | |
3001 | */ | |
3002 | selectTabs: function() { | |
3003 | if (location.hash) { | |
3004 | var $hash = location.hash.substr(1); | |
3005 | var $subIndex = null; | |
3006 | if (/-/.test(location.hash)) { | |
3007 | var $tmp = $hash.split('-'); | |
3008 | $hash = $tmp[0]; | |
3009 | $subIndex = $tmp[1]; | |
3010 | } | |
3011 | ||
3012 | // find a container which matches the first part | |
3013 | for (var $containerID in this._containers) { | |
3014 | var $tabMenu = this._containers[$containerID]; | |
3015 | if ($tabMenu.wcfTabs('hasAnchor', $hash, false)) { | |
3016 | if ($subIndex !== null) { | |
f24f0823 | 3017 | // try to find child tabMenu |
184a8d6d | 3018 | var $childTabMenu = $tabMenu.find('#' + $.wcfEscapeID($hash) + '.tabMenuContainer'); |
dbd319de AE |
3019 | if ($childTabMenu.length !== 1) { |
3020 | return; | |
3021 | } | |
3022 | ||
3023 | // validate match for second part | |
3024 | if (!$childTabMenu.wcfTabs('hasAnchor', $subIndex, true)) { | |
3025 | return; | |
f24f0823 AE |
3026 | } |
3027 | ||
dbd319de | 3028 | $childTabMenu.wcfTabs('select', $hash + '-' + $subIndex); |
f24f0823 | 3029 | } |
dbd319de AE |
3030 | |
3031 | $tabMenu.wcfTabs('select', $hash); | |
3032 | return; | |
3033 | } | |
f24f0823 AE |
3034 | } |
3035 | } | |
158bd3ca TD |
3036 | } |
3037 | }; | |
3038 | ||
3039 | /** | |
9f959ced MS |
3040 | * Templates that may be fetched more than once with different variables. |
3041 | * Based upon ideas from Prototype's template. | |
158bd3ca TD |
3042 | * |
3043 | * Usage: | |
3044 | * var myTemplate = new WCF.Template('{$hello} World'); | |
3045 | * myTemplate.fetch({ hello: 'Hi' }); // Hi World | |
3046 | * myTemplate.fetch({ hello: 'Hello' }); // Hello World | |
3047 | * | |
3048 | * my2ndTemplate = new WCF.Template('{@$html}{$html}'); | |
3049 | * my2ndTemplate.fetch({ html: '<b>Test</b>' }); // <b>Test</b><b>Test</b> | |
9f959ced | 3050 | * |
158bd3ca TD |
3051 | * var my3rdTemplate = new WCF.Template('You can use {literal}{$variable}{/literal}-Tags here'); |
3052 | * my3rdTemplate.fetch({ variable: 'Not shown' }); // You can use {$variable}-Tags here | |
3053 | * | |
158bd3ca TD |
3054 | * @param template template-content |
3055 | * @see https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/template.js | |
3056 | */ | |
39e27190 | 3057 | WCF.Template = Class.extend({ |
158bd3ca | 3058 | /** |
9f959ced | 3059 | * template content |
158bd3ca TD |
3060 | * @var string |
3061 | */ | |
3062 | _template: '', | |
3063 | ||
3064 | /** | |
9f959ced | 3065 | * saved literal tags |
158bd3ca TD |
3066 | * @var WCF.Dictionary |
3067 | */ | |
3068 | _literals: new WCF.Dictionary(), | |
3069 | ||
3070 | /** | |
3071 | * Prepares template | |
3072 | * | |
3073 | * @param $template template-content | |
3074 | */ | |
3075 | init: function($template) { | |
3076 | this._template = $template; | |
3077 | ||
3078 | // save literal-tags | |
3079 | this._template = this._template.replace(/\{literal\}(.*?)\{\/literal\}/g, $.proxy(function ($match) { | |
3080 | // hopefully no one uses this string in one of his templates | |
3081 | var id = '@@@@@@@@@@@'+Math.random()+'@@@@@@@@@@@'; | |
3082 | this._literals.add(id, $match.replace(/\{\/?literal\}/g, '')); | |
3083 | ||
3084 | return id; | |
3085 | }, this)); | |
3086 | }, | |
3087 | ||
3088 | /** | |
3089 | * Fetches the template with the given variables | |
3090 | * | |
3091 | * @param $variables variables to insert | |
3092 | * @return parsed template | |
3093 | */ | |
3094 | fetch: function($variables) { | |
3095 | var $result = this._template; | |
3096 | ||
3097 | // insert them :) | |
3098 | for (var $key in $variables) { | |
9fcc9ae5 | 3099 | $result = $result.replace(new RegExp(WCF.String.escapeRegExp('{$'+$key+'}'), 'g'), WCF.String.escapeHTML(new String($variables[$key]))); |
52560d9e | 3100 | $result = $result.replace(new RegExp(WCF.String.escapeRegExp('{#$'+$key+'}'), 'g'), WCF.String.formatNumeric($variables[$key])); |
158bd3ca TD |
3101 | $result = $result.replace(new RegExp(WCF.String.escapeRegExp('{@$'+$key+'}'), 'g'), $variables[$key]); |
3102 | } | |
3103 | ||
3104 | // insert delimiter tags | |
3105 | $result = $result.replace('{ldelim}', '{').replace('{rdelim}', '}'); | |
3106 | ||
3107 | // and re-insert saved literals | |
3108 | return this.insertLiterals($result); | |
3109 | }, | |
3110 | ||
3111 | /** | |
3112 | * Inserts literals into given string | |
3113 | * | |
3114 | * @param $template string to insert into | |
3115 | * @return string with inserted literals | |
3116 | */ | |
3117 | insertLiterals: function ($template) { | |
3118 | this._literals.each(function ($pair) { | |
3119 | $template = $template.replace($pair.key, $pair.value); | |
3120 | }); | |
3121 | ||
3122 | return $template; | |
3123 | }, | |
3124 | ||
3125 | /** | |
3126 | * Compiles this template into javascript-code | |
3127 | * | |
3128 | * @return WCF.Template.Compiled | |
3129 | */ | |
3130 | compile: function () { | |
3131 | var $compiled = this._template; | |
3132 | ||
3133 | // escape \ and ' | |
3134 | $compiled = $compiled.replace('\\', '\\\\').replace("'", "\\'"); | |
3135 | ||
3136 | // parse our variable-tags | |
3137 | $compiled = $compiled.replace(/\{\$(.*?)\}/g, function ($match) { | |
3138 | var $name = '$v.' + $match.substring(2, $match.length - 1); | |
3139 | // trinary operator to maintain compatibility with uncompiled template | |
3140 | // ($name) ? $name : '$match' | |
3141 | // -> $v.muh ? $v.muh : '{$muh}' | |
3142 | return "' + WCF.String.escapeHTML("+ $name + " ? " + $name + " : '" + $match + "') + '"; | |
52560d9e TD |
3143 | }).replace(/\{#\$(.*?)\}/g, function ($match) { |
3144 | var $name = '$v.' + $match.substring(3, $match.length - 1); | |
3145 | // trinary operator to maintain compatibility with uncompiled template | |
3146 | // ($name) ? $name : '$match' | |
3147 | // -> $v.muh ? $v.muh : '{$muh}' | |
3148 | return "' + WCF.String.formatNumeric("+ $name + " ? " + $name + " : '" + $match + "') + '"; | |
158bd3ca TD |
3149 | }).replace(/\{@\$(.*?)\}/g, function ($match) { |
3150 | var $name = '$v.' + $match.substring(3, $match.length - 1); | |
3151 | // trinary operator to maintain compatibility with uncompiled template | |
3152 | // ($name) ? $name : '$match' | |
3153 | // -> $v.muh ? $v.muh : '{$muh}' | |
3154 | return "' + ("+ $name + " ? " + $name + " : '" + $match + "') + '"; | |
3155 | }); | |
3156 | ||
3157 | // insert delimiter tags | |
3158 | $compiled = $compiled.replace('{ldelim}', '{').replace('{rdelim}', '}'); | |
3159 | ||
9df7f79d TD |
3160 | // escape newlines |
3161 | $compiled = $compiled.replace(/(\r\n|\n|\r)/g, '\\n'); | |
3162 | ||
158bd3ca TD |
3163 | // and re-insert saved literals |
3164 | return new WCF.Template.Compiled("'" + this.insertLiterals($compiled) + "';"); | |
3165 | } | |
39e27190 | 3166 | }); |
158bd3ca TD |
3167 | |
3168 | /** | |
3169 | * Represents a compiled template | |
3170 | * | |
3171 | * @param compiled compiled template | |
3172 | */ | |
39e27190 | 3173 | WCF.Template.Compiled = Class.extend({ |
158bd3ca TD |
3174 | /** |
3175 | * Compiled template | |
3176 | * | |
3177 | * @var string | |
3178 | */ | |
3179 | _compiled: '', | |
3180 | ||
3181 | /** | |
3182 | * Initializes our compiled template | |
3183 | * | |
3184 | * @param $compiled compiled template | |
3185 | */ | |
3186 | init: function($compiled) { | |
3187 | this._compiled = $compiled; | |
3188 | }, | |
3189 | ||
3190 | /** | |
3191 | * @see WCF.Template.fetch | |
3192 | */ | |
3193 | fetch: function($v) { | |
3194 | return eval(this._compiled); | |
3195 | } | |
39e27190 | 3196 | }); |
158bd3ca TD |
3197 | |
3198 | /** | |
3199 | * Toggles options. | |
3200 | * | |
3201 | * @param string element | |
3202 | * @param array showItems | |
3203 | * @param array hideItems | |
3204 | */ | |
39e27190 | 3205 | WCF.ToggleOptions = Class.extend({ |
158bd3ca TD |
3206 | /** |
3207 | * target item | |
3208 | * | |
3209 | * @var jQuery | |
3210 | */ | |
3211 | _element: null, | |
3212 | ||
3213 | /** | |
3214 | * list of items to be shown | |
3215 | * | |
3216 | * @var array | |
3217 | */ | |
3218 | _showItems: [], | |
3219 | ||
3220 | /** | |
3221 | * list of items to be hidden | |
3222 | * | |
3223 | * @var array | |
3224 | */ | |
3225 | _hideItems: [], | |
3226 | ||
3227 | /** | |
3228 | * Initializes option toggle. | |
3229 | * | |
3230 | * @param string element | |
3231 | * @param array showItems | |
3232 | * @param array hideItems | |
3233 | */ | |
3234 | init: function(element, showItems, hideItems) { | |
3235 | this._element = $('#' + element); | |
3236 | this._showItems = showItems; | |
3237 | this._hideItems = hideItems; | |
3238 | ||
3239 | // bind event | |
3240 | this._element.click($.proxy(this._toggle, this)); | |
3241 | ||
3242 | // execute toggle on init | |
3243 | this._toggle(); | |
3244 | }, | |
3245 | ||
3246 | /** | |
3247 | * Toggles items. | |
3248 | */ | |
3249 | _toggle: function() { | |
3250 | if (!this._element.attr('checked')) return; | |
3251 | ||
3252 | for (var $i = 0, $length = this._showItems.length; $i < $length; $i++) { | |
3253 | var $item = this._showItems[$i]; | |
3254 | ||
3255 | $('#' + $item).show(); | |
3256 | } | |
3257 | ||
3258 | for (var $i = 0, $length = this._hideItems.length; $i < $length; $i++) { | |
3259 | var $item = this._hideItems[$i]; | |
3260 | ||
3261 | $('#' + $item).hide(); | |
3262 | } | |
3263 | } | |
39e27190 | 3264 | }); |
158bd3ca | 3265 | |
1c746fe3 AE |
3266 | /** |
3267 | * Namespace for all kind of collapsible containers. | |
3268 | */ | |
3269 | WCF.Collapsible = {}; | |
3270 | ||
3271 | /** | |
3272 | * Simple implementation for collapsible content, neither does it | |
3273 | * store its state nor does it allow AJAX callbacks to fetch content. | |
3274 | */ | |
3275 | WCF.Collapsible.Simple = { | |
3276 | /** | |
3277 | * Initializes collapsibles. | |
3278 | */ | |
3279 | init: function() { | |
b29fbb53 | 3280 | $('.jsCollapsible').each($.proxy(function(index, button) { |
1c746fe3 AE |
3281 | this._initButton(button); |
3282 | }, this)); | |
3283 | }, | |
3284 | ||
3285 | /** | |
3286 | * Binds an event listener on all buttons triggering the collapsible. | |
3287 | * | |
3288 | * @param object button | |
3289 | */ | |
3290 | _initButton: function(button) { | |
3291 | var $button = $(button); | |
3292 | var $isOpen = $button.data('isOpen'); | |
3293 | ||
3294 | if (!$isOpen) { | |
3295 | // hide container on init | |
ec161ab2 | 3296 | $('#' + $button.data('collapsibleContainer')).hide(); |
1c746fe3 AE |
3297 | } |
3298 | ||
3299 | $button.click($.proxy(this._toggle, this)); | |
3300 | }, | |
3301 | ||
3302 | /** | |
3303 | * Toggles collapsible containers on click. | |
3304 | * | |
3305 | * @param object event | |
3306 | */ | |
3307 | _toggle: function(event) { | |
ec161ab2 | 3308 | var $button = $(event.currentTarget); |
1c746fe3 AE |
3309 | var $isOpen = $button.data('isOpen'); |
3310 | var $target = $('#' + $.wcfEscapeID($button.data('collapsibleContainer'))); | |
3311 | ||
3312 | if ($isOpen) { | |
3313 | $target.stop().wcfBlindOut('vertical', $.proxy(function() { | |
ac4f1e1d | 3314 | this._toggleImage($button, 'wcf.icon.closed'); |
1c746fe3 AE |
3315 | }, this)); |
3316 | $isOpen = false; | |
3317 | } | |
3318 | else { | |
3319 | $target.stop().wcfBlindIn('vertical', $.proxy(function() { | |
ac4f1e1d | 3320 | this._toggleImage($button, 'wcf.icon.opened'); |
1c746fe3 AE |
3321 | }, this)); |
3322 | $isOpen = true; | |
3323 | } | |
3324 | ||
3325 | $button.data('isOpen', $isOpen); | |
3326 | ||
3327 | // suppress event | |
3328 | event.stopPropagation(); | |
3329 | return false; | |
3330 | }, | |
3331 | ||
3332 | /** | |
3333 | * Toggles image of target button. | |
3334 | * | |
3335 | * @param jQuery button | |
3336 | * @param string image | |
3337 | */ | |
3338 | _toggleImage: function(button, image) { | |
3339 | var $icon = WCF.Icon.get(image); | |
3340 | var $image = button.find('img'); | |
3341 | ||
3342 | if ($image.length) { | |
3343 | $image.attr('src', $icon); | |
3344 | } | |
1c746fe3 AE |
3345 | } |
3346 | }; | |
3347 | ||
878d0d80 AE |
3348 | /** |
3349 | * Basic implementation for collapsible containers with AJAX support. Results for open | |
3350 | * and closed state will be cached. | |
9f959ced | 3351 | * |
878d0d80 AE |
3352 | * @param string className |
3353 | */ | |
3354 | WCF.Collapsible.Remote = Class.extend({ | |
3355 | /** | |
3356 | * class name | |
3357 | * @var string | |
3358 | */ | |
3359 | _className: '', | |
9f959ced | 3360 | |
878d0d80 AE |
3361 | /** |
3362 | * list of active containers | |
3363 | * @var object | |
3364 | */ | |
3365 | _containers: {}, | |
9f959ced | 3366 | |
878d0d80 AE |
3367 | /** |
3368 | * container meta data | |
3369 | * @var object | |
3370 | */ | |
3371 | _containerData: {}, | |
9f959ced | 3372 | |
878d0d80 AE |
3373 | /** |
3374 | * action proxy | |
3375 | * @var WCF.Action.Proxy | |
3376 | */ | |
3377 | _proxy: null, | |
9f959ced | 3378 | |
878d0d80 AE |
3379 | /** |
3380 | * Initializes the controller for collapsible containers with AJAX support. | |
3381 | * | |
3382 | * @param string className | |
3383 | */ | |
3384 | init: function(className) { | |
3385 | this._className = className; | |
3386 | ||
3387 | // validate containers | |
3388 | var $containers = this._getContainers(); | |
3389 | if ($containers.length == 0) { | |
3390 | console.debug('[WCF.Collapsible.Remote] Empty container set given, aborting.'); | |
3391 | } | |
3392 | ||
3393 | this._proxy = new WCF.Action.Proxy({ | |
3394 | success: $.proxy(this._success, this) | |
3395 | }); | |
3396 | ||
3397 | // initialize each container | |
3398 | $containers.each($.proxy(function(index, container) { | |
3399 | var $container = $(container); | |
c4b3ae32 AE |
3400 | var $containerID = $container.wcfIdentify(); |
3401 | this._containers[$containerID] = $container; | |
878d0d80 | 3402 | |
c4b3ae32 | 3403 | this._initContainer($containerID); |
878d0d80 AE |
3404 | }, this)); |
3405 | }, | |
3406 | ||
c4b3ae32 AE |
3407 | /** |
3408 | * Initializes a collapsible container. | |
3409 | * | |
3410 | * @param string containerID | |
3411 | */ | |
8805f7bf | 3412 | _initContainer: function(containerID) { |
77c43423 MW |
3413 | var $target = this._getTarget(containerID); |
3414 | var $buttonContainer = this._getButtonContainer(containerID); | |
3415 | var $button = this._createButton(containerID, $buttonContainer); | |
3416 | ||
3417 | // store container meta data | |
3418 | this._containerData[containerID] = { | |
3419 | button: $button, | |
3420 | buttonContainer: $buttonContainer, | |
c4b3ae32 | 3421 | isOpen: this._containers[containerID].data('isOpen'), |
77c43423 MW |
3422 | target: $target |
3423 | }; | |
3424 | }, | |
3425 | ||
878d0d80 AE |
3426 | /** |
3427 | * Returns a collection of collapsible containers. | |
3428 | * | |
3429 | * @return jQuery | |
3430 | */ | |
3431 | _getContainers: function() { }, | |
3432 | ||
3433 | /** | |
3434 | * Returns the target element for current collapsible container. | |
3435 | * | |
3436 | * @param integer containerID | |
3437 | * @return jQuery | |
3438 | */ | |
3439 | _getTarget: function(containerID) { }, | |
3440 | ||
3441 | /** | |
3442 | * Returns the button container for current collapsible container. | |
3443 | * | |
3444 | * @param integer containerID | |
3445 | * @return jQuery | |
3446 | */ | |
3447 | _getButtonContainer: function(containerID) { }, | |
3448 | ||
3449 | /** | |
3450 | * Creates the toggle button. | |
3451 | * | |
3452 | * @param integer containerID | |
3453 | * @param jQuery buttonContainer | |
3454 | */ | |
3455 | _createButton: function(containerID, buttonContainer) { | |
3456 | var $isOpen = this._containers[containerID].data('isOpen'); | |
15d36a35 | 3457 | var $button = $('<a class="collapsibleButton jsTooltip" title="'+WCF.Language.get('wcf.global.button.collapsible')+'"><img src="' + WCF.Icon.get('wcf.icon.' + ($isOpen ? 'opened' : 'closed')) + '" alt="" class="icon16" /></a>').prependTo(buttonContainer); |
878d0d80 | 3458 | $button.data('containerID', containerID).click($.proxy(this._toggleContainer, this)); |
9f959ced | 3459 | |
03812bbc | 3460 | return $button; |
878d0d80 AE |
3461 | }, |
3462 | ||
3463 | /** | |
3464 | * Toggles a container. | |
3465 | * | |
3466 | * @param object event | |
3467 | */ | |
3468 | _toggleContainer: function(event) { | |
b8a3ccb7 | 3469 | var $button = $(event.currentTarget); |
878d0d80 | 3470 | var $containerID = $button.data('containerID'); |
f4126129 | 3471 | var $isOpen = this._containerData[$containerID].isOpen; |
878d0d80 AE |
3472 | var $state = ($isOpen) ? 'open' : 'close'; |
3473 | var $newState = ($isOpen) ? 'close' : 'open'; | |
3474 | ||
878d0d80 AE |
3475 | // fetch content state via AJAX |
3476 | this._proxy.setOption('data', { | |
c4b3ae32 | 3477 | actionName: 'loadContainer', |
878d0d80 | 3478 | className: this._className, |
c4b3ae32 | 3479 | objectIDs: [ this._getObjectID($containerID) ], |
88a85f47 | 3480 | parameters: $.extend(true, { |
878d0d80 AE |
3481 | containerID: $containerID, |
3482 | currentState: $state, | |
a083bb38 | 3483 | newState: $newState |
88a85f47 | 3484 | }, this._getAdditionalParameters($containerID)) |
878d0d80 AE |
3485 | }); |
3486 | this._proxy.sendRequest(); | |
9f959ced | 3487 | |
03812bbc | 3488 | // set spinner for current button |
c4b3ae32 | 3489 | this._exchangeIcon($button); |
03812bbc | 3490 | }, |
c4b3ae32 AE |
3491 | |
3492 | /** | |
3493 | * Exchanges button icon. | |
3494 | * | |
3495 | * @param jQuery button | |
3496 | * @param string newIcon | |
3497 | */ | |
3498 | _exchangeIcon: function(button, newIcon) { | |
3499 | newIcon = newIcon || WCF.Icon.get('wcf.icon.loading'); | |
b8a3ccb7 | 3500 | button.find('img').attr('src', newIcon); |
878d0d80 AE |
3501 | }, |
3502 | ||
3503 | /** | |
3504 | * Returns the object id for current container. | |
3505 | * | |
3506 | * @param integer containerID | |
3507 | * @return integer | |
3508 | */ | |
eac3b734 MS |
3509 | _getObjectID: function(containerID) { |
3510 | return $('#' + containerID).data('objectID'); | |
3511 | }, | |
878d0d80 | 3512 | |
88a85f47 MW |
3513 | /** |
3514 | * Returns additional parameters. | |
3515 | * | |
3516 | * @param integer containerID | |
3517 | * @return object | |
3518 | */ | |
3519 | _getAdditionalParameters: function(containerID) { | |
3520 | return {}; | |
3521 | }, | |
3522 | ||
77c43423 MW |
3523 | /** |
3524 | * Updates container content. | |
3525 | * | |
3526 | * @param integer containerID | |
3527 | * @param string newContent | |
3528 | * @param string newState | |
3529 | */ | |
3530 | _updateContent: function(containerID, newContent, newState) { | |
3531 | this._containerData[containerID].target.html(newContent); | |
3532 | }, | |
3533 | ||
878d0d80 AE |
3534 | /** |
3535 | * Sets content upon successfull AJAX request. | |
3536 | * | |
3537 | * @param object data | |
3538 | * @param string textStatus | |
3539 | * @param jQuery jqXHR | |
3540 | */ | |
3541 | _success: function(data, textStatus, jqXHR) { | |
3542 | // validate container id | |
3543 | if (!data.returnValues.containerID) return; | |
3544 | var $containerID = data.returnValues.containerID; | |
3545 | ||
3546 | // check if container id is known | |
3547 | if (!this._containers[$containerID]) return; | |
3548 | ||
3549 | // update content storage | |
3550 | this._containerData[$containerID].isOpen = (data.returnValues.isOpen) ? true : false; | |
9fd51bd2 | 3551 | var $newState = (data.returnValues.isOpen) ? 'open' : 'close'; |
878d0d80 AE |
3552 | |
3553 | // update container content | |
77c43423 | 3554 | this._updateContent($containerID, data.returnValues.content, $newState); |
b8a3ccb7 MW |
3555 | |
3556 | // update icon | |
c4b3ae32 AE |
3557 | this._exchangeIcon(this._containerData[$containerID].button, WCF.Icon.get('wcf.icon.' + (data.returnValues.isOpen ? 'opened' : 'closed'))); |
3558 | } | |
3559 | }); | |
3560 | ||
3561 | /** | |
3562 | * Basic implementation for collapsible containers with AJAX support. Requires collapsible | |
3563 | * content to be available in DOM already, if you want to load content on the fly use | |
3564 | * WCF.Collapsible.Remote instead. | |
3565 | */ | |
3566 | WCF.Collapsible.SimpleRemote = WCF.Collapsible.Remote.extend({ | |
3567 | /** | |
3568 | * Initializes an AJAX-based collapsible handler. | |
3569 | * | |
3570 | * @param string className | |
3571 | */ | |
3572 | init: function(className) { | |
3573 | this._super(className); | |
3574 | ||
3575 | // override settings for action proxy | |
3576 | this._proxy = new WCF.Action.Proxy({ | |
3577 | showLoadingOverlay: false | |
3578 | }); | |
3579 | }, | |
3580 | ||
f19a9976 AE |
3581 | /** |
3582 | * @see WCF.Collapsible.Remote._initContainer() | |
3583 | */ | |
3584 | _initContainer: function(containerID) { | |
3585 | this._super(containerID); | |
3586 | ||
3587 | // hide container on init if applicable | |
3588 | if (!this._containerData[containerID].isOpen) { | |
3589 | this._containerData[containerID].target.hide(); | |
3590 | this._exchangeIcon(this._containerData[containerID].button, WCF.Icon.get('wcf.icon.closed')); | |
3591 | } | |
3592 | }, | |
3593 | ||
c4b3ae32 AE |
3594 | /** |
3595 | * Toggles container visibility. | |
3596 | * | |
3597 | * @param object event | |
3598 | */ | |
3599 | _toggleContainer: function(event) { | |
3600 | var $button = $(event.currentTarget); | |
3601 | var $containerID = $button.data('containerID'); | |
3602 | var $isOpen = this._containerData[$containerID].isOpen; | |
3603 | var $currentState = ($isOpen) ? 'open' : 'close'; | |
3604 | var $newState = ($isOpen) ? 'close' : 'open'; | |
3605 | ||
3606 | this._proxy.setOption('data', { | |
3607 | actionName: 'toggleContainer', | |
3608 | className: this._className, | |
3609 | objectIDs: [ this._getObjectID($containerID) ], | |
11c6026b | 3610 | parameters: $.extend(true, { |
c4b3ae32 AE |
3611 | containerID: $containerID, |
3612 | currentState: $currentState, | |
3613 | newState: $newState | |
11c6026b | 3614 | }, this._getAdditionalParameters($containerID)) |
c4b3ae32 AE |
3615 | }); |
3616 | this._proxy.sendRequest(); | |
3617 | ||
3618 | // exchange icon | |
3619 | this._exchangeIcon(this._containerData[$containerID].button, WCF.Icon.get('wcf.icon.' + ($newState === 'open' ? 'opened' : 'closed'))); | |
6ea6d38b AE |
3620 | |
3621 | // toggle container | |
3622 | if ($newState === 'open') { | |
3623 | this._containerData[$containerID].target.show(); | |
3624 | } | |
3625 | else { | |
3626 | this._containerData[$containerID].target.hide(); | |
3627 | } | |
3628 | ||
3629 | // update container data | |
3630 | this._containerData[$containerID].isOpen = ($newState === 'open' ? true : false); | |
878d0d80 AE |
3631 | } |
3632 | }); | |
9abd2c89 | 3633 | |
168a9753 AE |
3634 | /** |
3635 | * Provides collapsible sidebars with persistency support. | |
3636 | */ | |
9abd2c89 | 3637 | WCF.Collapsible.Sidebar = Class.extend({ |
168a9753 AE |
3638 | /** |
3639 | * trigger button object | |
3640 | * @var jQuery | |
3641 | */ | |
9abd2c89 | 3642 | _button: null, |
168a9753 | 3643 | |
a3574790 AE |
3644 | /** |
3645 | * trigger button height | |
3646 | * @var integer | |
3647 | */ | |
3648 | _buttonHeight: 0, | |
3649 | ||
168a9753 AE |
3650 | /** |
3651 | * sidebar state | |
3652 | * @var boolean | |
3653 | */ | |
9abd2c89 | 3654 | _isOpen: false, |
168a9753 | 3655 | |
a3574790 AE |
3656 | /** |
3657 | * main container object | |
3658 | * @var jQuery | |
3659 | */ | |
3660 | _mainContainer: null, | |
3661 | ||
168a9753 AE |
3662 | /** |
3663 | * action proxy | |
3664 | * @var WCF.Action.Proxy | |
3665 | */ | |
9abd2c89 | 3666 | _proxy: null, |
168a9753 AE |
3667 | |
3668 | /** | |
3669 | * sidebar object | |
3670 | * @var jQuery | |
3671 | */ | |
9abd2c89 | 3672 | _sidebar: null, |
168a9753 | 3673 | |
a3574790 AE |
3674 | /** |
3675 | * sidebar height | |
3676 | * @var integer | |
3677 | */ | |
3678 | _sidebarHeight: 0, | |
3679 | ||
168a9753 AE |
3680 | /** |
3681 | * sidebar identifier | |
3682 | * @var string | |
3683 | */ | |
9abd2c89 AE |
3684 | _sidebarName: '', |
3685 | ||
a3574790 AE |
3686 | /** |
3687 | * sidebar offset from document top | |
3688 | * @var integer | |
3689 | */ | |
3690 | _sidebarOffset: 0, | |
3691 | ||
3692 | /** | |
3693 | * user panel height | |
3694 | * @var integer | |
3695 | */ | |
3696 | _userPanelHeight: 0, | |
3697 | ||
168a9753 AE |
3698 | /** |
3699 | * Creates a new WCF.Collapsible.Sidebar object. | |
3700 | */ | |
9abd2c89 AE |
3701 | init: function() { |
3702 | this._sidebar = $('.sidebar:eq(0)'); | |
3703 | if (!this._sidebar.length) { | |
3704 | console.debug("[WCF.Collapsible.Sidebar] Could not find sidebar, aborting."); | |
3705 | return; | |
3706 | } | |
3707 | ||
a3574790 | 3708 | this._isOpen = (this._sidebar.data('isOpen')) ? true : false; |
9abd2c89 | 3709 | this._sidebarName = this._sidebar.data('sidebarName'); |
a3574790 AE |
3710 | this._mainContainer = $('#main'); |
3711 | this._sidebarHeight = this._sidebar.height(); | |
3712 | this._sidebarOffset = this._sidebar.getOffsets('offset').top; | |
3713 | this._userPanelHeight = $('#topMenu').outerHeight(); | |
9abd2c89 AE |
3714 | |
3715 | // add toggle button | |
7bf3204e | 3716 | WCF.DOMNodeInsertedHandler.enable(); |
9abd2c89 AE |
3717 | this._button = $('<a class="collapsibleButton jsTooltip" title="' + WCF.Language.get('wcf.global.button.collapsible') + '" />').prependTo(this._sidebar); |
3718 | this._button.click($.proxy(this._click, this)); | |
a3574790 | 3719 | this._buttonHeight = this._button.outerHeight(); |
7bf3204e | 3720 | WCF.DOMNodeInsertedHandler.disable(); |
9abd2c89 AE |
3721 | |
3722 | this._proxy = new WCF.Action.Proxy({ | |
a3574790 AE |
3723 | showLoadingOverlay: false, |
3724 | url: 'index.php/AJAXInvoke/?t=' + SECURITY_TOKEN + SID_ARG_2ND | |
9abd2c89 AE |
3725 | }); |
3726 | ||
a3574790 AE |
3727 | $(document).scroll($.proxy(this._scroll, this)).resize($.proxy(this._scroll, this)); |
3728 | ||
9abd2c89 | 3729 | this._renderSidebar(); |
a3574790 | 3730 | this._scroll(); |
9abd2c89 AE |
3731 | }, |
3732 | ||
168a9753 AE |
3733 | /** |
3734 | * Handles clicks on the trigger button. | |
3735 | */ | |
9abd2c89 AE |
3736 | _click: function() { |
3737 | this._isOpen = (this._isOpen) ? false : true; | |
3738 | ||
3739 | this._proxy.setOption('data', { | |
168a9753 AE |
3740 | actionName: 'toggle', |
3741 | className: 'wcf\\system\\user\\collapsible\\content\\UserCollapsibleSidebarHandler', | |
9abd2c89 AE |
3742 | isOpen: (this._isOpen ? 1 : 0), |
3743 | sidebarName: this._sidebarName | |
3744 | }); | |
3745 | this._proxy.sendRequest(); | |
3746 | ||
3747 | this._renderSidebar(); | |
3748 | }, | |
3749 | ||
a3574790 AE |
3750 | /** |
3751 | * Aligns the toggle button upon scroll or resize. | |
3752 | */ | |
3753 | _scroll: function() { | |
3754 | var $window = $(window); | |
3755 | var $scrollOffset = $window.scrollTop(); | |
3756 | ||
3757 | // calculate top and bottom coordinates of visible sidebar | |
3758 | var $topOffset = Math.max($scrollOffset - this._sidebarOffset, 0); | |
3759 | var $bottomOffset = Math.min(this._mainContainer.height(), ($window.height() + $scrollOffset) - this._sidebarOffset); | |
3760 | ||
3761 | var $buttonTop = 0; | |
3762 | if ($bottomOffset === $topOffset) { | |
3763 | // sidebar not within visible area | |
3764 | $buttonTop = this._sidebarOffset + this._sidebarHeight; | |
3765 | } | |
3766 | else { | |
3767 | $buttonTop = $topOffset + (($bottomOffset - $topOffset) / 2); | |
3768 | ||
3769 | // if the user panel is above the sidebar, substract it's height | |
3770 | var $overlap = Math.max(Math.min($topOffset - this._userPanelHeight, this._userPanelHeight), 0); | |
3771 | if ($overlap > 0) { | |
3772 | $buttonTop += ($overlap / 2); | |
3773 | } | |
3774 | } | |
3775 | ||
3776 | // ensure the button does not exceed bottom boundaries | |
3777 | if (($bottomOffset - $topOffset - this._userPanelHeight) < this._buttonHeight) { | |
3778 | $buttonTop = $buttonTop - this._buttonHeight; | |
3779 | } | |
3780 | else { | |
3781 | // exclude half button height | |
3782 | $buttonTop = Math.max($buttonTop - (this._buttonHeight / 2), 0); | |
3783 | } | |
3784 | ||
3785 | this._button.css({ top: $buttonTop + 'px' }); | |
3786 | ||
3787 | }, | |
3788 | ||
168a9753 AE |
3789 | /** |
3790 | * Renders the sidebar state. | |
3791 | */ | |
9abd2c89 AE |
3792 | _renderSidebar: function() { |
3793 | if (this._isOpen) { | |
a3574790 | 3794 | this._mainContainer.removeClass('sidebarCollapsed'); |
9abd2c89 AE |
3795 | } |
3796 | else { | |
a3574790 | 3797 | this._mainContainer.addClass('sidebarCollapsed'); |
9abd2c89 | 3798 | } |
f70b8234 AE |
3799 | |
3800 | // update button position | |
3801 | this._scroll(); | |
9abd2c89 AE |
3802 | } |
3803 | }); | |
878d0d80 | 3804 | |
fb969237 TD |
3805 | /** |
3806 | * Holds userdata of the current user | |
3807 | */ | |
3808 | WCF.User = { | |
3809 | /** | |
a99ebd63 | 3810 | * id of the active user |
fb969237 TD |
3811 | * @var integer |
3812 | */ | |
3813 | userID: 0, | |
3814 | ||
3815 | /** | |
9f959ced | 3816 | * name of the active user |
fb969237 TD |
3817 | * @var string |
3818 | */ | |
3819 | username: '', | |
3820 | ||
3821 | /** | |
3822 | * Initializes userdata | |
3823 | * | |
3824 | * @param integer userID | |
3825 | * @param string username | |
3826 | */ | |
3827 | init: function(userID, username) { | |
3828 | this.userID = userID; | |
3829 | this.username = username; | |
3830 | } | |
3831 | }; | |
3832 | ||
f84920f4 MW |
3833 | /** |
3834 | * Namespace for effect-related functions. | |
3835 | */ | |
3836 | WCF.Effect = {}; | |
3837 | ||
80e49fec AE |
3838 | /** |
3839 | * Scrolls to a specific element offset, optionally handling menu height. | |
3840 | */ | |
3841 | WCF.Effect.Scroll = Class.extend({ | |
3842 | /** | |
3843 | * Scrolls to a specific element offset. | |
3844 | * | |
3845 | * @param jQuery element | |
3846 | * @param boolean excludeMenuHeight | |
3847 | * @return boolean | |
3848 | */ | |
3849 | scrollTo: function(element, excludeMenuHeight) { | |
3850 | if (!element.length) { | |
3851 | return true; | |
3852 | } | |
3853 | ||
3854 | var $elementOffset = element.getOffsets().top; | |
3855 | var $documentHeight = $(document).height(); | |
3856 | var $windowHeight = $(window).height(); | |
3857 | ||
3858 | // handles menu height | |
3859 | if (excludeMenuHeight) { | |
3860 | $elementOffset = Math.max($elementOffset - $('#topMenu').outerHeight(), 0); | |
3861 | } | |
3862 | ||
3863 | if ($elementOffset > $documentHeight - $windowHeight) { | |
3864 | $elementOffset = $documentHeight - $windowHeight; | |
3865 | if ($elementOffset < 0) { | |
3866 | $elementOffset = 0; | |
3867 | } | |
3868 | } | |
3869 | ||
3870 | $('html,body').animate({ scrollTop: $elementOffset }, 400, function (x, t, b, c, d) { | |
3871 | return -c * ( ( t = t / d - 1 ) * t * t * t - 1) + b; | |
3872 | }); | |
3873 | ||
3874 | return false; | |
3875 | } | |
3876 | }); | |
3877 | ||
f84920f4 MW |
3878 | /** |
3879 | * Creates a smooth scroll effect. | |
3880 | */ | |
80e49fec | 3881 | WCF.Effect.SmoothScroll = WCF.Effect.Scroll.extend({ |
f84920f4 MW |
3882 | /** |
3883 | * Initializes effect. | |
3884 | */ | |
3885 | init: function() { | |
80e49fec | 3886 | var self = this; |
e0b74ad2 | 3887 | $(document).on('click', 'a[href$=#top],a[href$=#bottom]', function() { |
f84920f4 | 3888 | var $target = $(this.hash); |
80e49fec | 3889 | self.scrollTo($target, true); |
1b9cadb9 AE |
3890 | |
3891 | return false; | |
f84920f4 MW |
3892 | }); |
3893 | } | |
80e49fec | 3894 | }); |
f84920f4 | 3895 | |
c39544e5 | 3896 | /** |
8f3284a3 | 3897 | * Creates the balloon tool-tip. |
c39544e5 | 3898 | */ |
39e27190 | 3899 | WCF.Effect.BalloonTooltip = Class.extend({ |
e68f4d0d AE |
3900 | /** |
3901 | * initialization state | |
3902 | * @var boolean | |
3903 | */ | |
3904 | _didInit: false, | |
9f959ced | 3905 | |
e68f4d0d AE |
3906 | /** |
3907 | * tooltip element | |
3908 | * @var jQuery | |
3909 | */ | |
3910 | _tooltip: null, | |
9f959ced | 3911 | |
e68f4d0d AE |
3912 | /** |
3913 | * cache viewport dimensions | |
3914 | * @var object | |
3915 | */ | |
3916 | _viewportDimensions: { }, | |
9f959ced | 3917 | |
e68f4d0d AE |
3918 | /** |
3919 | * Initializes tooltips. | |
3920 | */ | |
c39544e5 | 3921 | init: function() { |
e68f4d0d AE |
3922 | if (!this._didInit) { |
3923 | // create empty div | |
184a8d6d | 3924 | this._tooltip = $('<div id="balloonTooltip" class="balloonTooltip"><span id="balloonTooltipText"></span><span class="pointer"><span></span></span></div>').appendTo($('body')).hide(); |
9f959ced | 3925 | |
e68f4d0d AE |
3926 | // get viewport dimensions |
3927 | this._updateViewportDimensions(); | |
9f959ced | 3928 | |
e68f4d0d AE |
3929 | // update viewport dimensions on resize |
3930 | $(window).resize($.proxy(this._updateViewportDimensions, this)); | |
9f959ced | 3931 | |
e68f4d0d | 3932 | // observe DOM changes |
4bf9feab | 3933 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Effect.BalloonTooltip', $.proxy(this.init, this)); |
9f959ced | 3934 | |
e68f4d0d AE |
3935 | this._didInit = true; |
3936 | } | |
3937 | ||
c39544e5 | 3938 | // init elements |
72065432 | 3939 | $('.jsTooltip').each($.proxy(this._initTooltip, this)); |
e68f4d0d | 3940 | }, |
9f959ced | 3941 | |
e68f4d0d AE |
3942 | /** |
3943 | * Updates cached viewport dimensions. | |
3944 | */ | |
3945 | _updateViewportDimensions: function() { | |
3946 | this._viewportDimensions = $(document).getDimensions(); | |
c39544e5 MW |
3947 | }, |
3948 | ||
e68f4d0d AE |
3949 | /** |
3950 | * Initializes a tooltip element. | |
3951 | * | |
3952 | * @param integer index | |
3953 | * @param object element | |
3954 | */ | |
3955 | _initTooltip: function(index, element) { | |
3956 | var $element = $(element); | |
c39544e5 | 3957 | |
72065432 L |
3958 | if ($element.hasClass('jsTooltip')) { |
3959 | $element.removeClass('jsTooltip'); | |
e68f4d0d | 3960 | var $title = $element.attr('title'); |
9f959ced | 3961 | |
e68f4d0d AE |
3962 | // ignore empty elements |
3963 | if ($title !== '') { | |
3964 | $element.data('tooltip', $title); | |
f4126129 | 3965 | $element.removeAttr('title'); |
9f959ced | 3966 | |
e68f4d0d AE |
3967 | $element.hover( |
3968 | $.proxy(this._mouseEnterHandler, this), | |
3969 | $.proxy(this._mouseLeaveHandler, this) | |
3970 | ); | |
8cfb8ca1 | 3971 | $element.click($.proxy(this._mouseLeaveHandler, this)); |
e68f4d0d AE |
3972 | } |
3973 | } | |
c39544e5 MW |
3974 | }, |
3975 | ||
e68f4d0d AE |
3976 | /** |
3977 | * Shows tooltip on hover. | |
3978 | * | |
3979 | * @param object event | |
3980 | */ | |
c39544e5 MW |
3981 | _mouseEnterHandler: function(event) { |
3982 | var $element = $(event.currentTarget); | |
c39544e5 | 3983 | |
93430cba | 3984 | var $title = $element.attr('title'); |
ea2294bc TD |
3985 | if ($title && $title !== '') { |
3986 | $element.data('tooltip', $title); | |
3987 | $element.removeAttr('title'); | |
3988 | } | |
93430cba | 3989 | |
8cfb8ca1 MW |
3990 | // reset tooltip position |
3991 | this._tooltip.css({ | |
3992 | top: "0px", | |
3993 | left: "0px" | |
3994 | }); | |
3995 | ||
e81ca6a1 MW |
3996 | // empty tooltip, skip |
3997 | if (!$element.data('tooltip')) { | |
3998 | this._tooltip.hide(); | |
3999 | return; | |
4000 | } | |
4001 | ||
e68f4d0d AE |
4002 | // update text |
4003 | this._tooltip.children('span:eq(0)').text($element.data('tooltip')); | |
9f959ced | 4004 | |
e68f4d0d | 4005 | // get arrow |
ef097134 | 4006 | var $arrow = this._tooltip.find('.pointer'); |
9f959ced | 4007 | |
e68f4d0d AE |
4008 | // get arrow width |
4009 | this._tooltip.show(); | |
4010 | var $arrowWidth = $arrow.outerWidth(); | |
4011 | this._tooltip.hide(); | |
24c42d67 | 4012 | |
e68f4d0d AE |
4013 | // calculate position |
4014 | var $elementOffsets = $element.getOffsets('offset'); | |
4015 | var $elementDimensions = $element.getDimensions('outer'); | |
4016 | var $tooltipDimensions = this._tooltip.getDimensions('outer'); | |
4017 | var $tooltipDimensionsInner = this._tooltip.getDimensions('inner'); | |
9f959ced | 4018 | |
e68f4d0d AE |
4019 | var $elementCenter = $elementOffsets.left + Math.ceil($elementDimensions.width / 2); |
4020 | var $tooltipHalfWidth = Math.ceil($tooltipDimensions.width / 2); | |
257248cd | 4021 | |
e68f4d0d | 4022 | // determine alignment |
9f959ced | 4023 | var $alignment = 'center'; |
e68f4d0d AE |
4024 | if (($elementCenter - $tooltipHalfWidth) < 5) { |
4025 | $alignment = 'left'; | |
4026 | } | |
4027 | else if ((this._viewportDimensions.width - 5) < ($elementCenter + $tooltipHalfWidth)) { | |
4028 | $alignment = 'right'; | |
1fe125ca | 4029 | } |
9f959ced | 4030 | |
e68f4d0d AE |
4031 | // calculate top offset |
4032 | var $top = $elementOffsets.top + $elementDimensions.height + 7; | |
9f959ced | 4033 | |
e68f4d0d AE |
4034 | // calculate left offset |
4035 | switch ($alignment) { | |
4036 | case 'center': | |
4037 | var $left = Math.round($elementOffsets.left - $tooltipHalfWidth + ($elementDimensions.width / 2)); | |
9f959ced | 4038 | |
e68f4d0d AE |
4039 | $arrow.css({ |
4040 | left: ($tooltipDimensionsInner.width / 2 - $arrowWidth / 2) + "px" | |
4041 | }); | |
4042 | break; | |
9f959ced | 4043 | |
e68f4d0d AE |
4044 | case 'left': |
4045 | var $left = $elementOffsets.left; | |
9f959ced | 4046 | |
e68f4d0d | 4047 | $arrow.css({ |
270a205a | 4048 | left: "5px" |
e68f4d0d AE |
4049 | }); |
4050 | break; | |
9f959ced | 4051 | |
e68f4d0d AE |
4052 | case 'right': |
4053 | var $left = $elementOffsets.left + $elementDimensions.width - $tooltipDimensions.width; | |
9f959ced | 4054 | |
e68f4d0d | 4055 | $arrow.css({ |
270a205a | 4056 | left: ($tooltipDimensionsInner.width - $arrowWidth - 5) + "px" |
e68f4d0d AE |
4057 | }); |
4058 | break; | |
4059 | } | |
9f959ced | 4060 | |
e68f4d0d AE |
4061 | // move tooltip |
4062 | this._tooltip.css({ | |
4063 | top: $top + "px", | |
4064 | left: $left + "px" | |
4065 | }); | |
4066 | ||
4067 | // show tooltip | |
90a8839c | 4068 | this._tooltip.wcfFadeIn(); |
2875507b MW |
4069 | }, |
4070 | ||
e68f4d0d AE |
4071 | /** |
4072 | * Hides tooltip once cursor left the element. | |
4073 | * | |
4074 | * @param object event | |
4075 | */ | |
2875507b | 4076 | _mouseLeaveHandler: function(event) { |
90a8839c MW |
4077 | this._tooltip.stop().hide().css({ |
4078 | opacity: 1 | |
4079 | }); | |
c39544e5 | 4080 | } |
39e27190 | 4081 | }); |
c39544e5 | 4082 | |
b3991cb3 AE |
4083 | /** |
4084 | * Handles clicks outside an overlay, hitting body-tag through bubbling. | |
4085 | * | |
4086 | * You should always remove callbacks before disposing the attached element, | |
4087 | * preventing errors from blocking the iteration. Furthermore you should | |
4088 | * always handle clicks on your overlay's container and return 'false' to | |
4089 | * prevent bubbling. | |
4090 | */ | |
4091 | WCF.CloseOverlayHandler = { | |
4092 | /** | |
4093 | * list of callbacks | |
4094 | * @var WCF.Dictionary | |
4095 | */ | |
4096 | _callbacks: new WCF.Dictionary(), | |
9f959ced | 4097 | |
b3991cb3 AE |
4098 | /** |
4099 | * indicates that overlay handler is listening to click events on body-tag | |
4100 | * @var boolean | |
4101 | */ | |
4102 | _isListening: false, | |
9f959ced | 4103 | |
b3991cb3 AE |
4104 | /** |
4105 | * Adds a new callback. | |
4106 | * | |
4107 | * @param string identifier | |
4108 | * @param object callback | |
4109 | */ | |
4110 | addCallback: function(identifier, callback) { | |
4111 | this._bindListener(); | |
83428b7f | 4112 | |
b3991cb3 | 4113 | if (this._callbacks.isset(identifier)) { |
78e4d558 | 4114 | console.debug("[WCF.CloseOverlayHandler] identifier '" + identifier + "' is already bound to a callback"); |
b3991cb3 AE |
4115 | return false; |
4116 | } | |
9f959ced | 4117 | |
b3991cb3 AE |
4118 | this._callbacks.add(identifier, callback); |
4119 | }, | |
9f959ced | 4120 | |
b3991cb3 AE |
4121 | /** |
4122 | * Removes a callback from list. | |
4123 | * | |
4124 | * @param string identifier | |
4125 | */ | |
4126 | removeCallback: function(identifier) { | |
4127 | if (this._callbacks.isset(identifier)) { | |
4128 | this._callbacks.remove(identifier); | |
4129 | } | |
4130 | }, | |
9f959ced | 4131 | |
b3991cb3 AE |
4132 | /** |
4133 | * Binds click event handler. | |
4134 | */ | |
4135 | _bindListener: function() { | |
4136 | if (this._isListening) return; | |
9f959ced | 4137 | |
b3991cb3 | 4138 | $('body').click($.proxy(this._executeCallbacks, this)); |
9f959ced | 4139 | |
b3991cb3 AE |
4140 | this._isListening = true; |
4141 | }, | |
9f959ced | 4142 | |
b3991cb3 AE |
4143 | /** |
4144 | * Executes callbacks on click. | |
4145 | */ | |
83428b7f | 4146 | _executeCallbacks: function(event) { |
b3991cb3 AE |
4147 | this._callbacks.each(function(pair) { |
4148 | // execute callback | |
4149 | pair.value(); | |
4150 | }); | |
4151 | } | |
4152 | }; | |
4153 | ||
9c1e5045 AE |
4154 | /** |
4155 | * Notifies objects once a DOM node was inserted. | |
4156 | */ | |
4157 | WCF.DOMNodeInsertedHandler = { | |
4158 | /** | |
4159 | * list of callbacks | |
4160 | * @var WCF.Dictionary | |
4161 | */ | |
4162 | _callbacks: new WCF.Dictionary(), | |
597e2774 AE |
4163 | |
4164 | /** | |
4165 | * true if DOMNodeInserted event should be ignored | |
4166 | * @var boolean | |
4167 | */ | |
4168 | _discardEvent: true, | |
4169 | ||
d371330f AE |
4170 | /** |
4171 | * counts requests to enable WCF.DOMNodeInsertedHandler | |
4172 | * @var integer | |
4173 | */ | |
4174 | _discardEventCount: 0, | |
4175 | ||
9c1e5045 AE |
4176 | /** |
4177 | * prevent infinite loop if a callback manipulates DOM | |
4178 | * @var boolean | |
4179 | */ | |
4180 | _isExecuting: false, | |
597e2774 | 4181 | |
9c1e5045 AE |
4182 | /** |
4183 | * indicates that overlay handler is listening to click events on body-tag | |
4184 | * @var boolean | |
4185 | */ | |
4186 | _isListening: false, | |
597e2774 | 4187 | |
9c1e5045 AE |
4188 | /** |
4189 | * Adds a new callback. | |
4190 | * | |
4191 | * @param string identifier | |
4192 | * @param object callback | |
4193 | */ | |
4194 | addCallback: function(identifier, callback) { | |
d371330f | 4195 | this._discardEventCount = 0; |
9c1e5045 | 4196 | this._bindListener(); |
597e2774 | 4197 | |
9c1e5045 | 4198 | if (this._callbacks.isset(identifier)) { |
78e4d558 | 4199 | console.debug("[WCF.DOMNodeInsertedHandler] identifier '" + identifier + "' is already bound to a callback"); |
9c1e5045 AE |
4200 | return false; |
4201 | } | |
597e2774 | 4202 | |
9c1e5045 AE |
4203 | this._callbacks.add(identifier, callback); |
4204 | }, | |
597e2774 | 4205 | |
9c1e5045 AE |
4206 | /** |
4207 | * Removes a callback from list. | |
4208 | * | |
4209 | * @param string identifier | |
4210 | */ | |
4211 | removeCallback: function(identifier) { | |
4212 | if (this._callbacks.isset(identifier)) { | |
4213 | this._callbacks.remove(identifier); | |
4214 | } | |
4215 | }, | |
597e2774 | 4216 | |
9c1e5045 AE |
4217 | /** |
4218 | * Binds click event handler. | |
4219 | */ | |
4220 | _bindListener: function() { | |
4221 | if (this._isListening) return; | |
597e2774 | 4222 | |
9c1e5045 | 4223 | $(document).bind('DOMNodeInserted', $.proxy(this._executeCallbacks, this)); |
597e2774 | 4224 | |
9c1e5045 AE |
4225 | this._isListening = true; |
4226 | }, | |
597e2774 | 4227 | |
9c1e5045 AE |
4228 | /** |
4229 | * Executes callbacks on click. | |
4230 | */ | |
d371330f | 4231 | _executeCallbacks: function() { |
597e2774 AE |
4232 | if (this._discardEvent || this._isExecuting) return; |
4233 | ||
901393e3 | 4234 | // do not track events while executing callbacks |
9c1e5045 | 4235 | this._isExecuting = true; |
901393e3 | 4236 | |
9c1e5045 AE |
4237 | this._callbacks.each(function(pair) { |
4238 | // execute callback | |
d371330f | 4239 | pair.value(); |
9c1e5045 | 4240 | }); |
901393e3 AE |
4241 | |
4242 | // enable listener again | |
4243 | this._isExecuting = false; | |
597e2774 AE |
4244 | }, |
4245 | ||
4246 | /** | |
4247 | * Disables DOMNodeInsertedHandler, should be used after you've enabled it. | |
4248 | */ | |
4249 | disable: function() { | |
d371330f AE |
4250 | this._discardEventCount--; |
4251 | ||
4252 | if (this._discardEventCount < 1) { | |
4253 | this._discardEvent = true; | |
4254 | this._discardEventCount = 0; | |
4255 | } | |
597e2774 AE |
4256 | }, |
4257 | ||
4258 | /** | |
4259 | * Enables DOMNodeInsertedHandler, should be used if you're inserting HTML (e.g. via AJAX) | |
4260 | * which might contain event-related elements. You have to disable the DOMNodeInsertedHandler | |
4261 | * once you've enabled it, if you fail it will cause an infinite loop! | |
4262 | */ | |
4263 | enable: function() { | |
d371330f AE |
4264 | this._discardEventCount++; |
4265 | ||
597e2774 | 4266 | this._discardEvent = false; |
d371330f AE |
4267 | }, |
4268 | ||
4269 | /** | |
4270 | * Forces execution of DOMNodeInsertedHandler. | |
4271 | */ | |
4272 | forceExecution: function() { | |
4273 | this.enable(); | |
4274 | this._executeCallbacks(); | |
4275 | this.disable(); | |
9c1e5045 AE |
4276 | } |
4277 | }; | |
4278 | ||
809e4170 MS |
4279 | /** |
4280 | * Notifies objects once a DOM node was removed. | |
4281 | */ | |
4282 | WCF.DOMNodeRemovedHandler = { | |
4283 | /** | |
4284 | * list of callbacks | |
4285 | * @var WCF.Dictionary | |
4286 | */ | |
4287 | _callbacks: new WCF.Dictionary(), | |
9f959ced | 4288 | |
809e4170 MS |
4289 | /** |
4290 | * prevent infinite loop if a callback manipulates DOM | |
4291 | * @var boolean | |
4292 | */ | |
4293 | _isExecuting: false, | |
9f959ced | 4294 | |
809e4170 MS |
4295 | /** |
4296 | * indicates that overlay handler is listening to DOMNodeRemoved events on body-tag | |
4297 | * @var boolean | |
4298 | */ | |
4299 | _isListening: false, | |
9f959ced | 4300 | |
809e4170 MS |
4301 | /** |
4302 | * Adds a new callback. | |
4303 | * | |
4304 | * @param string identifier | |
4305 | * @param object callback | |
4306 | */ | |
4307 | addCallback: function(identifier, callback) { | |
4308 | this._bindListener(); | |
9f959ced | 4309 | |
809e4170 MS |
4310 | if (this._callbacks.isset(identifier)) { |
4311 | console.debug("[WCF.DOMNodeRemovedHandler] identifier '" + identifier + "' is already bound to a callback"); | |
4312 | return false; | |
4313 | } | |
9f959ced | 4314 | |
809e4170 MS |
4315 | this._callbacks.add(identifier, callback); |
4316 | }, | |
9f959ced | 4317 | |
809e4170 MS |
4318 | /** |
4319 | * Removes a callback from list. | |
4320 | * | |
4321 | * @param string identifier | |
4322 | */ | |
4323 | removeCallback: function(identifier) { | |
4324 | if (this._callbacks.isset(identifier)) { | |
4325 | this._callbacks.remove(identifier); | |
4326 | } | |
4327 | }, | |
9f959ced | 4328 | |
809e4170 MS |
4329 | /** |
4330 | * Binds click event handler. | |
4331 | */ | |
4332 | _bindListener: function() { | |
4333 | if (this._isListening) return; | |
9f959ced | 4334 | |
809e4170 | 4335 | $(document).bind('DOMNodeRemoved', $.proxy(this._executeCallbacks, this)); |
9f959ced | 4336 | |
809e4170 MS |
4337 | this._isListening = true; |
4338 | }, | |
9f959ced | 4339 | |
809e4170 MS |
4340 | /** |
4341 | * Executes callbacks if a DOM node is removed. | |
4342 | */ | |
4343 | _executeCallbacks: function(event) { | |
4344 | if (this._isExecuting) return; | |
9f959ced | 4345 | |
809e4170 MS |
4346 | // do not track events while executing callbacks |
4347 | this._isExecuting = true; | |
4348 | ||
4349 | this._callbacks.each(function(pair) { | |
4350 | // execute callback | |
4351 | pair.value(event); | |
4352 | }); | |
4353 | ||
4354 | // enable listener again | |
4355 | this._isExecuting = false; | |
4356 | } | |
4357 | }; | |
4358 | ||
4359 | /** | |
4360 | * Namespace for table related classes. | |
4361 | */ | |
4362 | WCF.Table = {}; | |
4363 | ||
4364 | /** | |
4365 | * Handles empty tables which can be used in combination with WCF.Action.Proxy. | |
4366 | */ | |
4367 | WCF.Table.EmptyTableHandler = Class.extend({ | |
4368 | /** | |
4369 | * handler options | |
4370 | * @var object | |
4371 | */ | |
4372 | _options: {}, | |
4373 | ||
4374 | /** | |
4375 | * class name of the relevant rows | |
4376 | * @var string | |
4377 | */ | |
4378 | _rowClassName: '', | |
4379 | ||
4380 | /** | |
4381 | * Initalizes a new WCF.Table.EmptyTableHandler object. | |
4382 | * | |
809e4170 | 4383 | * @param jQuery tableContainer |
e0ee2eff | 4384 | * @param string rowClassName |
809e4170 MS |
4385 | * @param object options |
4386 | */ | |
e0ee2eff | 4387 | init: function(tableContainer, rowClassName, options) { |
809e4170 MS |
4388 | this._rowClassName = rowClassName; |
4389 | this._tableContainer = tableContainer; | |
4390 | ||
4391 | this._options = $.extend(true, { | |
4392 | emptyMessage: null, | |
c0bd9c8a | 4393 | messageType: 'info', |
809e4170 MS |
4394 | refreshPage: false, |
4395 | updatePageNumber: false | |
4396 | }, options || { }); | |
4397 | ||
4398 | WCF.DOMNodeRemovedHandler.addCallback('WCF.Table.EmptyTableHandler.' + rowClassName, $.proxy(this._remove, this)); | |
4399 | }, | |
4400 | ||
4401 | /** | |
4402 | * Handles the removal of a DOM node. | |
4403 | */ | |
4404 | _remove: function(event) { | |
4405 | var element = $(event.target); | |
4406 | ||
4407 | // check if DOM element is relevant | |
4408 | if (element.hasClass(this._rowClassName)) { | |
4409 | var tbody = element.parents('tbody:eq(0)'); | |
4410 | ||
4411 | // check if table will be empty if DOM node is removed | |
4412 | if (tbody.children('tr').length == 1) { | |
4413 | if (this._options.emptyMessage) { | |
4414 | // insert message | |
4415 | this._tableContainer.replaceWith($('<p />').addClass(this._options.messageType).text(this._options.emptyMessage)); | |
4416 | } | |
4417 | else if (this._options.refreshPage) { | |
4418 | // refresh page | |
4419 | if (this._options.updatePageNumber) { | |
4420 | // calculate the new page number | |
4421 | var pageNumberURLComponents = window.location.href.match(/(\?|&)pageNo=(\d+)/g); | |
4422 | if (pageNumberURLComponents) { | |
4423 | var currentPageNumber = pageNumberURLComponents[pageNumberURLComponents.length - 1].match(/\d+/g); | |
4424 | if (this._options.updatePageNumber > 0) { | |
4425 | currentPageNumber++; | |
4426 | } | |
4427 | else { | |
4428 | currentPageNumber--; | |
4429 | } | |
4430 | ||
4431 | window.location = window.location.href.replace(pageNumberURLComponents[pageNumberURLComponents.length - 1], pageNumberURLComponents[pageNumberURLComponents.length - 1][0] + 'pageNo=' + currentPageNumber); | |
4432 | } | |
4433 | } | |
4434 | else { | |
4435 | window.location.reload(); | |
4436 | } | |
4437 | } | |
4438 | else { | |
4439 | // simply remove the table container | |
4440 | this._tableContainer.remove(); | |
4441 | } | |
4442 | } | |
4443 | } | |
4444 | } | |
4445 | }); | |
4446 | ||
046e1292 AE |
4447 | /** |
4448 | * Namespace for search related classes. | |
4449 | */ | |
4450 | WCF.Search = {}; | |
4451 | ||
4452 | /** | |
4ad00d80 | 4453 | * Performs a quick search. |
046e1292 | 4454 | */ |
4ad00d80 | 4455 | WCF.Search.Base = Class.extend({ |
046e1292 AE |
4456 | /** |
4457 | * notification callback | |
4458 | * @var object | |
4459 | */ | |
4460 | _callback: null, | |
4ad00d80 | 4461 | |
4d10750a AE |
4462 | /** |
4463 | * class name | |
4464 | * @var string | |
4465 | */ | |
4ad00d80 | 4466 | _className: '', |
c000b08a | 4467 | |
cf55c379 AE |
4468 | /** |
4469 | * comma seperated list | |
4470 | * @var boolean | |
4471 | */ | |
4472 | _commaSeperated: false, | |
4473 | ||
c000b08a MS |
4474 | /** |
4475 | * list with values that are excluded from seaching | |
4476 | * @var array | |
4477 | */ | |
4478 | _excludedSearchValues: [], | |
cf55c379 | 4479 | |
9a130ab4 AE |
4480 | /** |
4481 | * count of available results | |
4482 | * @var integer | |
4483 | */ | |
4484 | _itemCount: 0, | |
4485 | ||
4486 | /** | |
4487 | * item index, -1 if none is selected | |
4488 | * @var integer | |
4489 | */ | |
4490 | _itemIndex: -1, | |
4491 | ||
046e1292 AE |
4492 | /** |
4493 | * result list | |
4494 | * @var jQuery | |
4495 | */ | |
4496 | _list: null, | |
cf55c379 AE |
4497 | |
4498 | /** | |
4499 | * old search string, used for comparison | |
4500 | * @var array<string> | |
4501 | */ | |
4502 | _oldSearchString: [ ], | |
4503 | ||
046e1292 AE |
4504 | /** |
4505 | * action proxy | |
4506 | * @var WCF.Action.Proxy | |
4507 | */ | |
4508 | _proxy: null, | |
cf55c379 | 4509 | |
046e1292 AE |
4510 | /** |
4511 | * search input field | |
4512 | * @var jQuery | |
4513 | */ | |
4514 | _searchInput: null, | |
4d10750a AE |
4515 | |
4516 | /** | |
4517 | * minimum search input length, MUST be 1 or higher | |
4518 | * @var integer | |
4519 | */ | |
fbe99f89 | 4520 | _triggerLength: 3, |
cf55c379 | 4521 | |
046e1292 AE |
4522 | /** |
4523 | * Initializes a new search. | |
4524 | * | |
4525 | * @param jQuery searchInput | |
4526 | * @param object callback | |
c000b08a | 4527 | * @param array excludedSearchValues |
cf55c379 | 4528 | * @param boolean commaSeperated |
80da9de3 | 4529 | * @param boolean showLoadingOverlay |
046e1292 | 4530 | */ |
80da9de3 | 4531 | init: function(searchInput, callback, excludedSearchValues, commaSeperated, showLoadingOverlay) { |
b0ce5298 | 4532 | if (callback !== null && callback !== undefined && !$.isFunction(callback)) { |
6f475a52 | 4533 | console.debug("[WCF.Search.Base] The given callback is invalid, aborting."); |
046e1292 AE |
4534 | return; |
4535 | } | |
b0ce5298 AE |
4536 | |
4537 | this._callback = (callback) ? callback : null; | |
fdd8763a | 4538 | this._excludedSearchValues = []; |
c000b08a MS |
4539 | if (excludedSearchValues) { |
4540 | this._excludedSearchValues = excludedSearchValues; | |
4541 | } | |
71662ae8 AE |
4542 | |
4543 | this._searchInput = $(searchInput); | |
4544 | if (!this._searchInput.length) { | |
4545 | console.debug("[WCF.Search.Base] Selector '" + searchInput + "' for search input is invalid, aborting."); | |
4546 | return; | |
4547 | } | |
4548 | ||
fbdfbaba | 4549 | this._searchInput.keydown($.proxy(this._keyDown, this)).keyup($.proxy(this._keyUp, this)).wrap('<span class="dropdown" />'); |
7f45f320 | 4550 | this._list = $('<ul class="dropdownMenu" />').insertAfter(this._searchInput); |
cf55c379 AE |
4551 | this._commaSeperated = (commaSeperated) ? true : false; |
4552 | this._oldSearchString = [ ]; | |
046e1292 | 4553 | |
9a130ab4 AE |
4554 | this._itemCount = 0; |
4555 | this._itemIndex = -1; | |
4556 | ||
046e1292 | 4557 | this._proxy = new WCF.Action.Proxy({ |
80da9de3 | 4558 | showLoadingOverlay: (showLoadingOverlay === false ? false : true), |
046e1292 AE |
4559 | success: $.proxy(this._success, this) |
4560 | }); | |
b0ce5298 AE |
4561 | |
4562 | if (this._searchInput.getTagName() === 'input') { | |
4563 | this._searchInput.attr('autocomplete', 'off'); | |
4564 | } | |
03e34cc3 AE |
4565 | |
4566 | this._searchInput.blur($.proxy(this._blur, this)); | |
4567 | }, | |
4568 | ||
4569 | /** | |
4570 | * Closes the dropdown after a short delay. | |
4571 | */ | |
4572 | _blur: function() { | |
4573 | var self = this; | |
4574 | new WCF.PeriodicalExecuter(function(pe) { | |
4575 | if (self._list.is(':visible')) { | |
4576 | self._clearList(false); | |
4577 | } | |
4578 | ||
4579 | pe.stop(); | |
4580 | }, 100); | |
046e1292 | 4581 | }, |
4ad00d80 | 4582 | |
fbdfbaba AE |
4583 | /** |
4584 | * Blocks execution of 'Enter' event. | |
4585 | * | |
4586 | * @param object event | |
4587 | */ | |
4588 | _keyDown: function(event) { | |
4589 | if (event.which === 13) { | |
4590 | event.preventDefault(); | |
4591 | } | |
4592 | }, | |
4593 | ||
046e1292 AE |
4594 | /** |
4595 | * Performs a search upon key up. | |
cf55c379 AE |
4596 | * |
4597 | * @param object event | |
046e1292 | 4598 | */ |
cf55c379 | 4599 | _keyUp: function(event) { |
9a130ab4 AE |
4600 | // handle arrow keys and return key |
4601 | switch (event.which) { | |
4602 | case 37: // arrow-left | |
4603 | case 39: // arrow-right | |
4604 | return; | |
4605 | break; | |
4606 | ||
4607 | case 38: // arrow up | |
4608 | this._selectPreviousItem(); | |
4609 | return; | |
4610 | break; | |
4611 | ||
4612 | case 40: // arrow down | |
4613 | this._selectNextItem(); | |
4614 | return; | |
4615 | break; | |
4616 | ||
4617 | case 13: // return key | |
4618 | return this._selectElement(event); | |
4619 | break; | |
4620 | } | |
4621 | ||
cf55c379 | 4622 | var $content = this._getSearchString(event); |
046e1292 AE |
4623 | if ($content === '') { |
4624 | this._clearList(true); | |
4625 | } | |
4d10750a | 4626 | else if ($content.length >= this._triggerLength) { |
4ad00d80 AE |
4627 | var $parameters = { |
4628 | data: { | |
c000b08a | 4629 | excludedSearchValues: this._excludedSearchValues, |
4ad00d80 AE |
4630 | searchString: $content |
4631 | } | |
4632 | }; | |
4633 | ||
046e1292 | 4634 | this._proxy.setOption('data', { |
a427a8c8 | 4635 | actionName: 'getSearchResultList', |
4ad00d80 AE |
4636 | className: this._className, |
4637 | parameters: this._getParameters($parameters) | |
046e1292 AE |
4638 | }); |
4639 | this._proxy.sendRequest(); | |
4640 | } | |
4d10750a AE |
4641 | else { |
4642 | // input below trigger length | |
4643 | this._clearList(false); | |
4644 | } | |
046e1292 | 4645 | }, |
4ad00d80 | 4646 | |
9a130ab4 AE |
4647 | /** |
4648 | * Selects the next item in list. | |
4649 | */ | |
4650 | _selectNextItem: function() { | |
4651 | if (this._itemCount === 0) { | |
4652 | return; | |
4653 | } | |
4654 | ||
4655 | // remove previous marking | |
4656 | this._itemIndex++; | |
4657 | if (this._itemIndex === this._itemCount) { | |
4658 | this._itemIndex = 0; | |
4659 | } | |
4660 | ||
4661 | this._highlightSelectedElement(); | |
4662 | }, | |
4663 | ||
4664 | /** | |
4665 | * Selects the previous item in list. | |
4666 | */ | |
4667 | _selectPreviousItem: function() { | |
4668 | if (this._itemCount === 0) { | |
4669 | return; | |
4670 | } | |
4671 | ||
4672 | this._itemIndex--; | |
4673 | if (this._itemIndex === -1) { | |
4674 | this._itemIndex = this._itemCount - 1; | |
4675 | } | |
4676 | ||
4677 | this._highlightSelectedElement(); | |
4678 | }, | |
4679 | ||
4680 | /** | |
4681 | * Highlights the active item. | |
4682 | */ | |
4683 | _highlightSelectedElement: function() { | |
4684 | this._list.find('li').removeClass('dropdownNavigationItem'); | |
4685 | this._list.find('li:eq(' + this._itemIndex + ')').addClass('dropdownNavigationItem'); | |
4686 | }, | |
4687 | ||
4688 | /** | |
4689 | * Selects the active item by pressing the return key. | |
4690 | * | |
4691 | * @param object event | |
4692 | * @return boolean | |
4693 | */ | |
4694 | _selectElement: function(event) { | |
4695 | if (this._itemCount === 0) { | |
4696 | return true; | |
4697 | } | |
4698 | ||
4699 | this._list.find('li.dropdownNavigationItem').trigger('click'); | |
4700 | ||
4701 | return false; | |
4702 | }, | |
4703 | ||
cf55c379 AE |
4704 | /** |
4705 | * Returns search string. | |
4706 | * | |
4707 | * @return string | |
4708 | */ | |
4709 | _getSearchString: function(event) { | |
4710 | var $searchString = $.trim(this._searchInput.val()); | |
4711 | if (this._commaSeperated) { | |
4712 | var $keyCode = event.keyCode || event.which; | |
4713 | if ($keyCode == 188) { | |
4714 | // ignore event if char is 188 = , | |
4715 | return ''; | |
4716 | } | |
4717 | ||
4718 | var $current = $searchString.split(','); | |
68097635 AE |
4719 | var $length = $current.length; |
4720 | for (var $i = 0; $i < $length; $i++) { | |
cf55c379 AE |
4721 | // remove whitespaces at the beginning or end |
4722 | $current[$i] = $.trim($current[$i]); | |
68097635 AE |
4723 | } |
4724 | ||
4725 | for (var $i = 0; $i < $length; $i++) { | |
cf55c379 AE |
4726 | var $part = $current[$i]; |
4727 | ||
4728 | if (this._oldSearchString[$i]) { | |
4729 | // compare part | |
4730 | if ($part != this._oldSearchString[$i]) { | |
4731 | // current part was changed | |
4732 | $searchString = $part; | |
4733 | break; | |
4734 | } | |
4735 | } | |
4736 | else { | |
4737 | // new part was added | |
4738 | $searchString = $part; | |
4739 | break; | |
4740 | } | |
4741 | } | |
4742 | ||
4743 | this._oldSearchString = $current; | |
4744 | } | |
4745 | ||
4746 | return $searchString; | |
4747 | }, | |
4748 | ||
4ad00d80 AE |
4749 | /** |
4750 | * Returns parameters for quick search. | |
4751 | * | |
4752 | * @param object parameters | |
4753 | * @return object | |
4754 | */ | |
4755 | _getParameters: function(parameters) { | |
4756 | return parameters; | |
4757 | }, | |
9f959ced | 4758 | |
046e1292 AE |
4759 | /** |
4760 | * Evalutes search results. | |
4761 | * | |
4762 | * @param object data | |
4763 | * @param string textStatus | |
4764 | * @param jQuery jqXHR | |
4765 | */ | |
4766 | _success: function(data, textStatus, jqXHR) { | |
7f45f320 AE |
4767 | this._clearList(false); |
4768 | ||
4769 | // no items available, abort | |
046e1292 | 4770 | if (!$.getLength(data.returnValues)) { |
046e1292 AE |
4771 | return; |
4772 | } | |
4ad00d80 | 4773 | |
046e1292 AE |
4774 | for (var $i in data.returnValues) { |
4775 | var $item = data.returnValues[$i]; | |
9f959ced | 4776 | |
4ad00d80 | 4777 | this._createListItem($item); |
046e1292 | 4778 | } |
4ad00d80 | 4779 | |
7f45f320 | 4780 | this._list.parent().addClass('dropdownOpen'); |
71662ae8 | 4781 | WCF.Dropdown.setAlignment(undefined, this._list); |
7f45f320 | 4782 | |
d27e2667 | 4783 | WCF.CloseOverlayHandler.addCallback('WCF.Search.Base', $.proxy(function() { this._clearList(true); }, this)); |
046e1292 | 4784 | }, |
4ad00d80 AE |
4785 | |
4786 | /** | |
4787 | * Creates a new list item. | |
4788 | * | |
4789 | * @param object item | |
4790 | * @return jQuery | |
4791 | */ | |
4792 | _createListItem: function(item) { | |
1c93e6e7 AE |
4793 | var $listItem = $('<li><span>' + item.label + '</span></li>').appendTo(this._list); |
4794 | $listItem.data('objectID', item.objectID).data('label', item.label).click($.proxy(this._executeCallback, this)); | |
4ad00d80 | 4795 | |
9a130ab4 AE |
4796 | this._itemCount++; |
4797 | ||
4ad00d80 AE |
4798 | return $listItem; |
4799 | }, | |
9f959ced | 4800 | |
046e1292 AE |
4801 | /** |
4802 | * Executes callback upon result click. | |
4803 | * | |
4804 | * @param object event | |
4805 | */ | |
4806 | _executeCallback: function(event) { | |
b0ce5298 | 4807 | var $clearSearchInput = false; |
046e1292 | 4808 | var $listItem = $(event.currentTarget); |
046e1292 | 4809 | // notify callback |
cf55c379 AE |
4810 | if (this._commaSeperated) { |
4811 | // auto-complete current part | |
4812 | var $result = $listItem.data('label'); | |
4813 | for (var $i = 0, $length = this._oldSearchString.length; $i < $length; $i++) { | |
4814 | var $part = this._oldSearchString[$i]; | |
47573ca9 | 4815 | if ($result.toLowerCase().indexOf($part.toLowerCase()) === 0) { |
cf55c379 AE |
4816 | this._oldSearchString[$i] = $result; |
4817 | this._searchInput.attr('value', this._oldSearchString.join(', ')); | |
4818 | ||
4819 | if ($.browser.webkit) { | |
4820 | // chrome won't display the new value until the textarea is rendered again | |
4821 | // this quick fix forces chrome to render it again, even though it changes nothing | |
4822 | this._searchInput.css({ display: 'block' }); | |
4823 | } | |
4824 | ||
68097635 | 4825 | // set focus on input field again |
47573ca9 | 4826 | var $position = this._searchInput.val().toLowerCase().indexOf($result.toLowerCase()) + $result.length; |
68097635 AE |
4827 | this._searchInput.focus().setCaret($position); |
4828 | ||
cf55c379 AE |
4829 | break; |
4830 | } | |
4831 | } | |
4832 | } | |
4833 | else { | |
b0ce5298 AE |
4834 | if (this._callback === null) { |
4835 | this._searchInput.val($listItem.data('label')); | |
4836 | } | |
4837 | else { | |
4838 | $clearSearchInput = (this._callback($listItem.data()) === true) ? true : false; | |
4839 | } | |
cf55c379 | 4840 | } |
9f959ced | 4841 | |
046e1292 | 4842 | // close list and revert input |
f28efd50 | 4843 | this._clearList($clearSearchInput); |
046e1292 | 4844 | }, |
9f959ced | 4845 | |
046e1292 AE |
4846 | /** |
4847 | * Closes the suggestion list and clears search input on demand. | |
4848 | * | |
4849 | * @param boolean clearSearchInput | |
4850 | */ | |
4851 | _clearList: function(clearSearchInput) { | |
cf55c379 | 4852 | if (clearSearchInput && !this._commaSeperated) { |
046e1292 AE |
4853 | this._searchInput.val(''); |
4854 | } | |
9f959ced | 4855 | |
7f45f320 AE |
4856 | this._list.parent().removeClass('dropdownOpen').end().empty(); |
4857 | ||
4858 | WCF.CloseOverlayHandler.removeCallback('WCF.Search.Base'); | |
9a130ab4 AE |
4859 | |
4860 | // reset item navigation | |
4861 | this._itemCount = 0; | |
4862 | this._itemIndex = -1; | |
c000b08a MS |
4863 | }, |
4864 | ||
4865 | /** | |
4866 | * Adds an excluded search value. | |
4867 | * | |
4868 | * @param string value | |
4869 | */ | |
4870 | addExcludedSearchValue: function(value) { | |
4871 | if (!WCF.inArray(value, this._excludedSearchValues)) { | |
4872 | this._excludedSearchValues.push(value); | |
4873 | } | |
4874 | }, | |
4875 | ||
4876 | /** | |
4877 | * Adds an excluded search value. | |
4878 | * | |
4879 | * @param string value | |
4880 | */ | |
4881 | removeExcludedSearchValue: function(value) { | |
4882 | var index = $.inArray(value, this._excludedSearchValues); | |
4883 | if (index != -1) { | |
4884 | this._excludedSearchValues.splice(index, 1); | |
4885 | } | |
046e1292 AE |
4886 | } |
4887 | }); | |
4888 | ||
4ad00d80 AE |
4889 | /** |
4890 | * Provides quick search for users and user groups. | |
4891 | * | |
4892 | * @see WCF.Search.Base | |
4893 | */ | |
4894 | WCF.Search.User = WCF.Search.Base.extend({ | |
4895 | /** | |
4896 | * @see WCF.Search.Base._className | |
4897 | */ | |
4898 | _className: 'wcf\\data\\user\\UserAction', | |
4899 | ||
4900 | /** | |
4901 | * include user groups in search | |
4902 | * @var boolean | |
4903 | */ | |
4904 | _includeUserGroups: false, | |
4905 | ||
4906 | /** | |
cf55c379 | 4907 | * @see WCF.Search.Base.init() |
4ad00d80 | 4908 | */ |
cf55c379 | 4909 | init: function(searchInput, callback, includeUserGroups, excludedSearchValues, commaSeperated) { |
4ad00d80 AE |
4910 | this._includeUserGroups = includeUserGroups; |
4911 | ||
cf55c379 | 4912 | this._super(searchInput, callback, excludedSearchValues, commaSeperated); |
4ad00d80 AE |
4913 | }, |
4914 | ||
4915 | /** | |
4916 | * @see WCF.Search.Base._getParameters() | |
4917 | */ | |
4918 | _getParameters: function(parameters) { | |
4ccbb0bb | 4919 | parameters.data.includeUserGroups = this._includeUserGroups ? 1 : 0; |
1c93e6e7 AE |
4920 | |
4921 | return parameters; | |
4ad00d80 AE |
4922 | }, |
4923 | ||
4924 | /** | |
4925 | * @see WCF.Search.Base._createListItem() | |
4926 | */ | |
4927 | _createListItem: function(item) { | |
4928 | var $listItem = this._super(item); | |
4929 | ||
4930 | // insert item type | |
fbe99f89 | 4931 | if (this._includeUserGroups) $('<img src="' + WCF.Icon.get('wcf.icon.user' + (item.type == 'group' ? 's' : '')) + '" alt="" class="icon16" style="margin-right: 4px;" />').prependTo($listItem.children('span:eq(0)')); |
4ad00d80 AE |
4932 | $listItem.data('type', item.type); |
4933 | ||
4934 | return $listItem; | |
4935 | } | |
4936 | }); | |
4937 | ||
5568e009 AE |
4938 | /** |
4939 | * Namespace for system-related classes. | |
4940 | */ | |
4941 | WCF.System = { }; | |
4942 | ||
4943 | /** | |
4944 | * System notification overlays. | |
4945 | * | |
4946 | * @param string message | |
4947 | * @param string cssClassNames | |
4948 | */ | |
4949 | WCF.System.Notification = Class.extend({ | |
4950 | /** | |
4951 | * callback on notification close | |
4952 | * @var object | |
4953 | */ | |
4954 | _callback: null, | |
4955 | ||
d2e126c6 AE |
4956 | /** |
4957 | * CSS class names | |
4958 | * @var string | |
4959 | */ | |
4960 | _cssClassNames: '', | |
4961 | ||
4962 | /** | |
4963 | * notification message | |
4964 | * @var string | |
4965 | */ | |
4966 | _message: '', | |
4967 | ||
5568e009 AE |
4968 | /** |
4969 | * notification overlay | |
4970 | * @var jQuery | |
4971 | */ | |
4972 | _overlay: null, | |
4973 | ||
4974 | /** | |
4975 | * Creates a new system notification overlay. | |
4976 | * | |
4977 | * @param string message | |
4978 | * @param string cssClassNames | |
4979 | */ | |
4980 | init: function(message, cssClassNames) { | |
d2e126c6 AE |
4981 | this._cssClassNames = cssClassNames || 'success'; |
4982 | this._message = message; | |
7c1fa02a | 4983 | this._overlay = $('#systemNotification'); |
5568e009 | 4984 | |
d2e126c6 AE |
4985 | if (!this._overlay.length) { |
4986 | this._overlay = $('<div id="systemNotification"><p></p></div>').appendTo(document.body); | |
5568e009 AE |
4987 | } |
4988 | }, | |
4989 | ||
4990 | /** | |
4991 | * Shows the notification overlay. | |
4992 | * | |
4993 | * @param object callback | |
4994 | * @param integer duration | |
7c1fa02a AE |
4995 | * @param string message |
4996 | * @param string cssClassName | |
5568e009 | 4997 | */ |
7c1fa02a | 4998 | show: function(callback, duration, message, cssClassNames) { |
5568e009 AE |
4999 | duration = parseInt(duration); |
5000 | if (!duration) duration = 2000; | |
5001 | ||
5002 | if (callback && $.isFunction(callback)) { | |
5003 | this._callback = callback; | |
5004 | } | |
5005 | ||
d2e126c6 AE |
5006 | this._overlay.children('p').html((message || this._message)); |
5007 | this._overlay.children('p').removeClass().addClass((cssClassNames || this._cssClassNames)); | |
7c1fa02a | 5008 | |
5568e009 AE |
5009 | // hide overlay after specified duration |
5010 | new WCF.PeriodicalExecuter($.proxy(this._hide, this), duration); | |
5011 | ||
5012 | this._overlay.addClass('open'); | |
5013 | }, | |
5014 | ||
5015 | /** | |
5016 | * Hides the notification overlay after executing the callback. | |
5017 | * | |
5018 | * @param WCF.PeriodicalExecuter pe | |
5019 | */ | |
5020 | _hide: function(pe) { | |
5021 | if (this._callback !== null) { | |
5022 | this._callback(); | |
5023 | } | |
5024 | ||
5025 | this._overlay.removeClass('open'); | |
5026 | ||
5027 | pe.stop(); | |
5028 | } | |
5029 | }); | |
5030 | ||
34e158d9 AE |
5031 | /** |
5032 | * Provides dialog-based confirmations. | |
5033 | */ | |
5034 | WCF.System.Confirmation = { | |
5035 | /** | |
5036 | * notification callback | |
5037 | * @var object | |
5038 | */ | |
5039 | _callback: null, | |
5040 | ||
5041 | /** | |
5042 | * confirmation dialog | |
5043 | * @var jQuery | |
5044 | */ | |
5045 | _dialog: null, | |
5046 | ||
f02b3f75 AE |
5047 | /** |
5048 | * callback parameters | |
5049 | * @var object | |
5050 | */ | |
5051 | _parameters: null, | |
5052 | ||
34e158d9 AE |
5053 | /** |
5054 | * dialog visibility | |
5055 | * @var boolean | |
5056 | */ | |
5057 | _visible: false, | |
5058 | ||
5059 | /** | |
5060 | * Displays a confirmation dialog. | |
5061 | * | |
5062 | * @param string message | |
5063 | * @param object callback | |
f02b3f75 | 5064 | * @param object parameters |
73b84427 | 5065 | * @param jQuery template |
34e158d9 | 5066 | */ |
73b84427 | 5067 | show: function(message, callback, parameters, template) { |
34e158d9 AE |
5068 | if (this._visible) { |
5069 | console.debug('[WCF.System.Confirmation] Confirmation dialog is already open, refusing action.'); | |
5070 | return; | |
5071 | } | |
5072 | ||
5073 | if (!$.isFunction(callback)) { | |
5074 | console.debug('[WCF.System.Confirmation] Given callback is invalid, aborting.'); | |
5075 | return; | |
5076 | } | |
5077 | ||
5078 | this._callback = callback; | |
f02b3f75 | 5079 | this._parameters = parameters; |
e730016d AE |
5080 | |
5081 | var $render = true; | |
34e158d9 AE |
5082 | if (this._dialog === null) { |
5083 | this._createDialog(); | |
e730016d | 5084 | $render = false; |
34e158d9 AE |
5085 | } |
5086 | ||
73b84427 AE |
5087 | this._dialog.find('#wcfSystemConfirmationContent').empty().hide(); |
5088 | if (template && template.length) { | |
5089 | template.appendTo(this._dialog.find('#wcfSystemConfirmationContent').show()); | |
5090 | } | |
5091 | ||
34e158d9 AE |
5092 | this._dialog.find('p').html(message); |
5093 | this._dialog.wcfDialog({ | |
5094 | onClose: $.proxy(this._close, this), | |
5095 | onShow: $.proxy(this._show, this), | |
5096 | title: WCF.Language.get('wcf.global.confirmation.title') | |
5097 | }); | |
e730016d AE |
5098 | if ($render) { |
5099 | this._dialog.wcfDialog('render'); | |
5100 | } | |
34e158d9 AE |
5101 | |
5102 | this._visible = true; | |
5103 | }, | |
5104 | ||
5105 | /** | |
5106 | * Creates the confirmation dialog on first use. | |
5107 | */ | |
5108 | _createDialog: function() { | |
73b84427 | 5109 | this._dialog = $('<div id="wcfSystemConfirmation" class="systemConfirmation"><p /><div id="wcfSystemConfirmationContent" /></div>').hide().appendTo(document.body); |
582a1874 | 5110 | var $formButtons = $('<div class="formSubmit" />').appendTo(this._dialog); |
34e158d9 | 5111 | |
582a1874 | 5112 | $('<button class="buttonPrimary">' + WCF.Language.get('wcf.global.confirmation.confirm') + '</button>').data('action', 'confirm').click($.proxy(this._click, this)).appendTo($formButtons); |
34e158d9 AE |
5113 | $('<button>' + WCF.Language.get('wcf.global.confirmation.cancel') + '</button>').data('action', 'cancel').click($.proxy(this._click, this)).appendTo($formButtons); |
5114 | }, | |
5115 | ||
5116 | /** | |
5117 | * Handles button clicks. | |
5118 | * | |
5119 | * @param object event | |
5120 | */ | |
5121 | _click: function(event) { | |
5122 | this._notify($(event.currentTarget).data('action')); | |
5123 | }, | |
5124 | ||
5125 | /** | |
5126 | * Handles dialog being closed. | |
5127 | */ | |
5128 | _close: function() { | |
5129 | if (this._visible) { | |
5130 | this._notify('cancel'); | |
5131 | } | |
5132 | }, | |
5133 | ||
5134 | /** | |
5135 | * Notifies callback upon user's decision. | |
5136 | * | |
5137 | * @param string action | |
5138 | */ | |
5139 | _notify: function(action) { | |
5140 | this._visible = false; | |
5141 | this._dialog.wcfDialog('close'); | |
5142 | ||
f02b3f75 | 5143 | this._callback(action, this._parameters); |
34e158d9 AE |
5144 | }, |
5145 | ||
5146 | /** | |
5147 | * Tries to set focus on confirm button. | |
5148 | */ | |
5149 | _show: function() { | |
582a1874 | 5150 | this._dialog.find('button.buttonPrimary').blur().focus(); |
34e158d9 AE |
5151 | } |
5152 | }; | |
5153 | ||
d83e246c AE |
5154 | /** |
5155 | * Provides the 'jump to page' overlay. | |
5156 | */ | |
5157 | WCF.System.PageNavigation = { | |
5158 | /** | |
5159 | * submit button | |
5160 | * @var jQuery | |
5161 | */ | |
5162 | _button: null, | |
5163 | ||
5164 | /** | |
5165 | * page No description | |
5166 | * @var jQuery | |
5167 | */ | |
5168 | _description: null, | |
5169 | ||
5170 | /** | |
5171 | * dialog overlay | |
5172 | * @var jQuery | |
5173 | */ | |
5174 | _dialog: null, | |
5175 | ||
5176 | /** | |
5177 | * active element id | |
5178 | * @var string | |
5179 | */ | |
5180 | _elementID: '', | |
5181 | ||
5182 | /** | |
5183 | * list of tracked navigation bars | |
5184 | * @var object | |
5185 | */ | |
5186 | _elements: { }, | |
5187 | ||
5188 | /** | |
5189 | * page No input | |
5190 | * @var jQuery | |
5191 | */ | |
5192 | _pageNo: null, | |
5193 | ||
5194 | /** | |
5195 | * Initializes the 'jump to page' overlay for given selector. | |
5196 | * | |
5197 | * @param string selector | |
3bdc6920 | 5198 | * @param object callback |
d83e246c | 5199 | */ |
3bdc6920 | 5200 | init: function(selector, callback) { |
d83e246c AE |
5201 | var $elements = $(selector); |
5202 | if (!$elements.length) { | |
5203 | return; | |
5204 | } | |
5205 | ||
3bdc6920 | 5206 | callback = callback || null; |
3bdc6920 AE |
5207 | if (callback !== null && !$.isFunction(callback)) { |
5208 | console.debug("[WCF.System.PageNavigation] Callback for selector '" + selector + "' is invalid, aborting."); | |
5209 | return; | |
5210 | } | |
5211 | ||
5212 | this._initElements($elements, callback); | |
d83e246c AE |
5213 | }, |
5214 | ||
5215 | /** | |
5216 | * Initializes the 'jump to page' overlay for given elements. | |
5217 | * | |
5218 | * @param jQuery elements | |
3bdc6920 | 5219 | * @param object callback |
d83e246c | 5220 | */ |
3bdc6920 | 5221 | _initElements: function(elements, callback) { |
d83e246c AE |
5222 | var self = this; |
5223 | elements.each(function(index, element) { | |
5224 | var $element = $(element); | |
a19475d3 | 5225 | console.debug($element.data()); |
d83e246c AE |
5226 | var $elementID = $element.wcfIdentify(); |
5227 | if (self._elements[$elementID] === undefined) { | |
5228 | self._elements[$elementID] = $element; | |
5229 | $element.find('li.jumpTo').data('elementID', $elementID).click($.proxy(self._click, self)); | |
5230 | } | |
3bdc6920 | 5231 | }).data('callback', callback); |
d83e246c AE |
5232 | }, |
5233 | ||
5234 | /** | |
5235 | * Shows the 'jump to page' overlay. | |
5236 | * | |
5237 | * @param object event | |
5238 | */ | |
5239 | _click: function(event) { | |
5240 | this._elementID = $(event.currentTarget).data('elementID'); | |
5241 | ||
5242 | if (this._dialog === null) { | |
5243 | this._dialog = $('<div id="pageNavigationOverlay" />').hide().appendTo(document.body); | |
5244 | ||
5245 | var $fieldset = $('<fieldset><legend>' + WCF.Language.get('wcf.global.page.jumpTo') + '</legend></fieldset>').appendTo(this._dialog); | |
5246 | $('<dl><dt><label for="jsPageNavigationPageNo">' + WCF.Language.get('wcf.global.page.jumpTo') + '</label></dt><dd></dd></dl>').appendTo($fieldset); | |
5247 | this._pageNo = $('<input type="number" id="jsPageNavigationPageNo" value="1" min="1" max="1" class="long" />').keyup($.proxy(this._keyUp, this)).appendTo($fieldset.find('dd')); | |
5248 | this._description = $('<small></small>').insertAfter(this._pageNo); | |
5249 | var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog); | |
5250 | this._button = $('<button class="buttonPrimary">' + WCF.Language.get('wcf.global.button.submit') + '</button>').click($.proxy(this._submit, this)).appendTo($formSubmit); | |
5251 | } | |
5252 | ||
5253 | this._button.enable(); | |
5254 | this._description.html(WCF.Language.get('wcf.global.page.jumpTo.description').replace(/#pages#/, this._elements[this._elementID].data('pages'))); | |
5255 | this._pageNo.val('1').attr('max', this._elements[this._elementID].data('pages')); | |
5256 | ||
5257 | this._dialog.wcfDialog({ | |
5258 | 'title': WCF.Language.get('wcf.global.page.pageNavigation') | |
5259 | }); | |
5260 | }, | |
5261 | ||
5262 | /** | |
5263 | * Validates the page No input. | |
5264 | */ | |
5265 | _keyUp: function() { | |
5266 | var $pageNo = parseInt(this._pageNo.val()) || 0; | |
5267 | if ($pageNo < 1 || $pageNo > this._pageNo.attr('max')) { | |
5268 | this._button.disable(); | |
5269 | } | |
5270 | else { | |
5271 | this._button.enable(); | |
5272 | } | |
5273 | }, | |
5274 | ||
5275 | /** | |
5276 | * Redirects to given page No. | |
5277 | */ | |
5278 | _submit: function() { | |
3bdc6920 AE |
5279 | var $pageNavigation = this._elements[this._elementID]; |
5280 | if ($pageNavigation.data('callback') === null) { | |
5281 | var $redirectURL = $pageNavigation.data('link').replace(/pageNo=%d/, 'pageNo=' + this._pageNo.val()); | |
5282 | window.location = $redirectURL; | |
5283 | } | |
5284 | else { | |
5285 | $pageNavigation.data('callback')(this._pageNo.val()); | |
a19475d3 | 5286 | this._dialog.wcfDialog('close'); |
3bdc6920 | 5287 | } |
d83e246c AE |
5288 | } |
5289 | }; | |
5290 | ||
dd932bc6 AE |
5291 | /** |
5292 | * Sends periodical requests to protect the session from expiring. By default | |
5293 | * it will send a request 1 minute before it would expire. | |
5294 | * | |
5295 | * @param integer seconds | |
5296 | */ | |
5297 | WCF.System.KeepAlive = Class.extend({ | |
5298 | /** | |
5299 | * Initializes the WCF.System.KeepAlive class. | |
5300 | * | |
5301 | * @param integer seconds | |
5302 | */ | |
5303 | init: function(seconds) { | |
5304 | new WCF.PeriodicalExecuter(function() { | |
5305 | new WCF.Action.Proxy({ | |
5306 | autoSend: true, | |
5307 | data: { | |
5308 | actionName: 'keepAlive', | |
5309 | className: 'wcf\\data\\session\\SessionAction' | |
90b713ac AE |
5310 | }, |
5311 | showLoadingOverlay: false | |
dd932bc6 AE |
5312 | }); |
5313 | }, (seconds * 1000)); | |
5314 | } | |
5315 | }); | |
5316 | ||
e7f87db1 AE |
5317 | /** |
5318 | * Default implementation for inline editors. | |
be8e935b AE |
5319 | * |
5320 | * @param string elementSelector | |
e7f87db1 AE |
5321 | */ |
5322 | WCF.InlineEditor = Class.extend({ | |
5323 | /** | |
5324 | * list of registered callbacks | |
5325 | * @var array<object> | |
5326 | */ | |
5327 | _callbacks: [ ], | |
5328 | ||
7c1fa02a AE |
5329 | /** |
5330 | * list of dropdown selections | |
5331 | * @var object | |
5332 | */ | |
5333 | _dropdowns: { }, | |
5334 | ||
e7f87db1 AE |
5335 | /** |
5336 | * list of container elements | |
5337 | * @var object | |
5338 | */ | |
5339 | _elements: { }, | |
5340 | ||
5341 | /** | |
7c1fa02a AE |
5342 | * notification object |
5343 | * @var WCF.System.Notification | |
e7f87db1 | 5344 | */ |
7c1fa02a | 5345 | _notification: null, |
e7f87db1 | 5346 | |
e7f87db1 AE |
5347 | /** |
5348 | * list of known options | |
5349 | * @var array<object> | |
5350 | */ | |
5351 | _options: [ ], | |
5352 | ||
5353 | /** | |
5354 | * action proxy | |
5355 | * @var WCF.Action.Proxy | |
5356 | */ | |
5357 | _proxy: null, | |
5358 | ||
f3ca0ddf AE |
5359 | /** |
5360 | * list of data to update upon success | |
5361 | * @var array<object> | |
5362 | */ | |
5363 | _updateData: [ ], | |
5364 | ||
e7f87db1 AE |
5365 | /** |
5366 | * Initializes a new inline editor. | |
5367 | */ | |
f3ca0ddf AE |
5368 | init: function(elementSelector) { |
5369 | var $elements = $(elementSelector); | |
e7f87db1 AE |
5370 | if (!$elements.length) { |
5371 | return; | |
5372 | } | |
5373 | ||
5374 | var self = this; | |
5375 | $elements.each(function(index, element) { | |
5376 | var $element = $(element); | |
5377 | var $elementID = $element.wcfIdentify(); | |
5378 | ||
5379 | // find trigger element | |
f3ca0ddf | 5380 | var $trigger = self._getTriggerElement($element); |
e7f87db1 AE |
5381 | if ($trigger === null || $trigger.length !== 1) { |
5382 | return; | |
5383 | } | |
5384 | ||
f3ca0ddf | 5385 | $trigger.click($.proxy(self._show, self)).data('elementID', $elementID); |
e7f87db1 AE |
5386 | |
5387 | // store reference | |
5388 | self._elements[$elementID] = $element; | |
5389 | }); | |
5390 | ||
5391 | this._proxy = new WCF.Action.Proxy({ | |
f3ca0ddf | 5392 | success: $.proxy(this._success, this) |
e7f87db1 | 5393 | }); |
f3ca0ddf AE |
5394 | |
5395 | this._setOptions(); | |
83428b7f AE |
5396 | |
5397 | WCF.CloseOverlayHandler.addCallback('WCF.InlineEditor', $.proxy(this._closeAll, this)); | |
7c1fa02a AE |
5398 | |
5399 | this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success'); | |
83428b7f AE |
5400 | }, |
5401 | ||
5402 | /** | |
5403 | * Closes all inline editors. | |
5404 | */ | |
5405 | _closeAll: function() { | |
5406 | for (var $elementID in this._elements) { | |
5407 | this._hide($elementID); | |
5408 | } | |
e7f87db1 AE |
5409 | }, |
5410 | ||
f3ca0ddf AE |
5411 | /** |
5412 | * Sets options for this inline editor. | |
5413 | */ | |
5414 | _setOptions: function() { | |
5415 | this._options = [ ]; | |
be8e935b | 5416 | }, |
f3ca0ddf | 5417 | |
e7f87db1 AE |
5418 | /** |
5419 | * Register an option callback for validation and execution. | |
5420 | * | |
5421 | * @param object callback | |
5422 | */ | |
5423 | registerCallback: function(callback) { | |
5424 | if ($.isFunction(callback)) { | |
5425 | this._callbacks.push(callback); | |
5426 | } | |
5427 | }, | |
5428 | ||
5429 | /** | |
5430 | * Returns the triggering element. | |
5431 | * | |
5432 | * @param jQuery element | |
5433 | * @return jQuery | |
5434 | */ | |
5435 | _getTriggerElement: function(element) { | |
5436 | return null; | |
5437 | }, | |
5438 | ||
5439 | /** | |
5440 | * Shows a dropdown menu if options are available. | |
5441 | * | |
5442 | * @param object event | |
5443 | */ | |
5444 | _show: function(event) { | |
5445 | var $elementID = $(event.currentTarget).data('elementID'); | |
f24f0823 | 5446 | |
e7f87db1 AE |
5447 | // build drop down |
5448 | if (!this._dropdowns[$elementID]) { | |
7c1fa02a AE |
5449 | var $trigger = this._getTriggerElement(this._elements[$elementID]).addClass('dropdownToggle').wrap('<span class="dropdown" />'); |
5450 | var $dropdown = $trigger.parent('span'); | |
8f13d83e | 5451 | $trigger.data('target', $dropdown.wcfIdentify()); |
7c1fa02a | 5452 | this._dropdowns[$elementID] = $('<ul class="dropdownMenu" style="top: ' + ($dropdown.outerHeight() + 14) + 'px;" />').insertAfter($trigger); |
e7f87db1 | 5453 | } |
84abb173 | 5454 | this._dropdowns[$elementID].empty(); |
e7f87db1 AE |
5455 | |
5456 | // validate options | |
5457 | var $hasOptions = false; | |
b3524514 | 5458 | var $lastElementType = ''; |
e7f87db1 AE |
5459 | for (var $i = 0, $length = this._options.length; $i < $length; $i++) { |
5460 | var $option = this._options[$i]; | |
5461 | ||
7c1fa02a | 5462 | if ($option.optionName === 'divider') { |
b3524514 AE |
5463 | if ($lastElementType !== '' && $lastElementType !== 'divider') { |
5464 | $('<li class="dropdownDivider" />').appendTo(this._dropdowns[$elementID]); | |
5465 | $lastElementType = $option.optionName; | |
5466 | } | |
7c1fa02a AE |
5467 | } |
5468 | else if (this._validate($elementID, $option.optionName) || this._validateCallbacks($elementID, $option.optionName)) { | |
e7f87db1 AE |
5469 | var $listItem = $('<li><span>' + $option.label + '</span></li>').appendTo(this._dropdowns[$elementID]); |
5470 | $listItem.data('elementID', $elementID).data('optionName', $option.optionName).click($.proxy(this._click, this)); | |
5471 | ||
5472 | $hasOptions = true; | |
b3524514 | 5473 | $lastElementType = $option.optionName; |
e7f87db1 AE |
5474 | } |
5475 | } | |
5476 | ||
5477 | if ($hasOptions) { | |
7c1fa02a | 5478 | this._dropdowns[$elementID].parent('span').addClass('dropdownOpen'); |
e7f87db1 | 5479 | } |
83428b7f AE |
5480 | |
5481 | return false; | |
e7f87db1 AE |
5482 | }, |
5483 | ||
5484 | /** | |
5485 | * Validates an option. | |
5486 | * | |
5487 | * @param string elementID | |
5488 | * @param string optionName | |
5489 | * @returns boolean | |
5490 | */ | |
5491 | _validate: function(elementID, optionName) { | |
5492 | return false; | |
5493 | }, | |
5494 | ||
5495 | /** | |
5496 | * Validates an option provided by callbacks. | |
5497 | * | |
5498 | * @param string elementID | |
5499 | * @param string optionName | |
5500 | * @return boolean | |
5501 | */ | |
5502 | _validateCallbacks: function(elementID, optionName) { | |
5503 | var $length = this._callbacks.length; | |
5504 | if ($length) { | |
5505 | for (var $i = 0; $i < $length; $i++) { | |
5506 | if (this._callbacks[$i].validate(this._elements[elementID], optionName)) { | |
5507 | return true; | |
5508 | } | |
5509 | } | |
5510 | } | |
5511 | ||
5512 | return false; | |
5513 | }, | |
5514 | ||
5515 | /** | |
5516 | * Handles AJAX responses. | |
5517 | * | |
5518 | * @param object data | |
5519 | * @param string textStatus | |
5520 | * @param jQuery jqXHR | |
5521 | */ | |
f3ca0ddf AE |
5522 | _success: function(data, textStatus, jqXHR) { |
5523 | var $length = this._updateData.length; | |
5524 | if (!$length) { | |
5525 | return; | |
5526 | } | |
5527 | ||
5528 | this._updateState(); | |
5529 | ||
5530 | this._updateData = [ ]; | |
5531 | }, | |
5532 | ||
5533 | /** | |
5534 | * Update element states based upon update data. | |
5535 | */ | |
5536 | _updateState: function() { }, | |
e7f87db1 AE |
5537 | |
5538 | /** | |
5539 | * Handles clicks within dropdown. | |
5540 | * | |
5541 | * @param object event | |
5542 | */ | |
5543 | _click: function(event) { | |
5544 | var $listItem = $(event.currentTarget); | |
5545 | var $elementID = $listItem.data('elementID'); | |
5546 | var $optionName = $listItem.data('optionName'); | |
5547 | ||
5548 | if (!this._execute($elementID, $optionName)) { | |
5549 | this._executeCallback($elementID, $optionName); | |
5550 | } | |
5551 | ||
5552 | this._hide($elementID); | |
5553 | }, | |
5554 | ||
5555 | /** | |
5556 | * Executes actions associated with an option. | |
5557 | * | |
5558 | * @param string elementID | |
5559 | * @param string optionName | |
5560 | * @return boolean | |
5561 | */ | |
5562 | _execute: function(elementID, optionName) { | |
5563 | return false; | |
5564 | }, | |
5565 | ||
5566 | /** | |
5567 | * Executes actions associated with an option provided by callbacks. | |
5568 | * | |
5569 | * @param string elementID | |
5570 | * @param string optionName | |
5571 | * @return boolean | |
5572 | */ | |
5573 | _executeCallback: function(elementID, optionName) { | |
5574 | var $length = this._callbacks.length; | |
5575 | if ($length) { | |
5576 | for (var $i = 0; $i < $length; $i++) { | |
5577 | if (this._callbacks[$i].execute(this._elements[elementID], optionName)) { | |
5578 | return true; | |
5579 | } | |
5580 | } | |
5581 | } | |
5582 | ||
5583 | return false; | |
5584 | }, | |
5585 | ||
5586 | /** | |
5587 | * Hides a dropdown menu. | |
5588 | * | |
5589 | * @param string elementID | |
5590 | */ | |
5591 | _hide: function(elementID) { | |
83428b7f | 5592 | if (this._dropdowns[elementID]) { |
7c1fa02a | 5593 | this._dropdowns[elementID].empty().parent('span').removeClass('dropdownOpen'); |
83428b7f | 5594 | } |
e7f87db1 AE |
5595 | } |
5596 | }); | |
5597 | ||
c1aaf7a7 MW |
5598 | /** |
5599 | * Default implementation for ajax file uploads | |
5600 | * | |
5601 | * @param jquery buttonSelector | |
5602 | * @param jquery fileListSelector | |
5603 | * @param string className | |
5604 | * @param jquery options | |
5605 | */ | |
5606 | WCF.Upload = Class.extend({ | |
5607 | /** | |
5608 | * name of the upload field | |
9f959ced | 5609 | * @var string |
c1aaf7a7 MW |
5610 | */ |
5611 | _name: '__files[]', | |
5612 | ||
5613 | /** | |
5614 | * button selector | |
9f959ced | 5615 | * @var jQuery |
c1aaf7a7 MW |
5616 | */ |
5617 | _buttonSelector: null, | |
5618 | ||
5619 | /** | |
5620 | * file list selector | |
9f959ced | 5621 | * @var jQuery |
c1aaf7a7 MW |
5622 | */ |
5623 | _fileListSelector: null, | |
5624 | ||
5625 | /** | |
5626 | * upload file | |
9f959ced | 5627 | * @var jQuery |
c1aaf7a7 MW |
5628 | */ |
5629 | _fileUpload: null, | |
5630 | ||
5631 | /** | |
5632 | * class name | |
9f959ced | 5633 | * @var string |
c1aaf7a7 MW |
5634 | */ |
5635 | _className: '', | |
5636 | ||
5637 | /** | |
5638 | * additional options | |
9f959ced | 5639 | * @var jQuery |
c1aaf7a7 MW |
5640 | */ |
5641 | _options: {}, | |
5642 | ||
5643 | /** | |
5644 | * upload matrix | |
9f959ced | 5645 | * @var array |
c1aaf7a7 MW |
5646 | */ |
5647 | _uploadMatrix: [], | |
5648 | ||
5649 | /** | |
9f959ced MS |
5650 | * true, if the active user's browser supports ajax file uploads |
5651 | * @var boolean | |
c1aaf7a7 MW |
5652 | */ |
5653 | _supportsAJAXUpload: true, | |
5654 | ||
5655 | /** | |
5656 | * fallback overlay for stupid browsers | |
9f959ced | 5657 | * @var jquery |
c1aaf7a7 MW |
5658 | */ |
5659 | _overlay: null, | |
5660 | ||
5661 | /** | |
5662 | * Initializes a new upload handler. | |
5663 | */ | |
5664 | init: function(buttonSelector, fileListSelector, className, options) { | |
5665 | this._buttonSelector = buttonSelector; | |
5666 | this._fileListSelector = fileListSelector; | |
5667 | this._className = className; | |
5668 | this._options = $.extend(true, { | |
5669 | action: 'upload', | |
5670 | multiple: false, | |
5671 | url: 'index.php/AJAXUpload/?t=' + SECURITY_TOKEN + SID_ARG_2ND | |
5672 | }, options); | |
5673 | ||
5674 | // check for ajax upload support | |
5675 | var $xhr = new XMLHttpRequest(); | |
5676 | this._supportsAJAXUpload = ($xhr && ('upload' in $xhr) && ('onprogress' in $xhr.upload)); | |
5677 | ||
5678 | // create upload button | |
5679 | this._createButton(); | |
5680 | }, | |
5681 | ||
5682 | /** | |
5683 | * Creates the upload button. | |
5684 | */ | |
5685 | _createButton: function() { | |
5686 | if (this._supportsAJAXUpload) { | |
a8c57470 | 5687 | this._fileUpload = $('<input type="file" name="'+this._name+'" '+(this._options.multiple ? 'multiple="true" ' : '')+'/>'); |
c1aaf7a7 | 5688 | this._fileUpload.change($.proxy(this._upload, this)); |
b18bb1d5 | 5689 | var $button = $('<p class="button uploadButton"><span>'+WCF.Language.get('wcf.global.button.upload')+'</span></p>'); |
c1aaf7a7 MW |
5690 | $button.append(this._fileUpload); |
5691 | } | |
5692 | else { | |
a8c57470 | 5693 | var $button = $('<p class="button"><span>Upload</span></p>'); |
c1aaf7a7 MW |
5694 | $button.click($.proxy(this._showOverlay, this)); |
5695 | } | |
5696 | ||
5697 | this._insertButton($button); | |
5698 | }, | |
5699 | ||
5700 | /** | |
5701 | * Inserts the upload button. | |
5702 | */ | |
5703 | _insertButton: function(button) { | |
5704 | this._buttonSelector.append(button); | |
5705 | }, | |
5706 | ||
5707 | /** | |
5708 | * Callback for file uploads. | |
5709 | */ | |
5710 | _upload: function() { | |
5711 | var $files = this._fileUpload.prop('files'); | |
5712 | ||
5713 | if ($files.length > 0) { | |
5714 | var $fd = new FormData(); | |
5715 | var self = this; | |
5716 | var $uploadID = this._uploadMatrix.length; | |
5717 | this._uploadMatrix[$uploadID] = []; | |
5718 | ||
5719 | for (var $i = 0; $i < $files.length; $i++) { | |
32f6cd95 MW |
5720 | var $li = this._initFile($files[$i]); |
5721 | $li.data('filename', $files[$i].name); | |
5722 | this._uploadMatrix[$uploadID].push($li); | |
c1aaf7a7 MW |
5723 | $fd.append('__files[]', $files[$i]); |
5724 | } | |
5725 | $fd.append('actionName', this._options.action); | |
5726 | $fd.append('className', this._className); | |
32f6cd95 MW |
5727 | var $additionalParameters = this._getParameters(); |
5728 | for (var $name in $additionalParameters) { | |
5729 | $fd.append('parameters['+$name+']', $additionalParameters[$name]); | |
5730 | } | |
c1aaf7a7 MW |
5731 | |
5732 | $.ajax({ | |
5733 | type: 'POST', | |
5734 | url: this._options.url, | |
5735 | enctype: 'multipart/form-data', | |
5736 | data: $fd, | |
5737 | contentType: false, | |
5738 | processData: false, | |
32f6cd95 MW |
5739 | success: function(data, textStatus, jqXHR) { |
5740 | self._success($uploadID, data); | |
5741 | }, | |
c1aaf7a7 MW |
5742 | error: $.proxy(this._error, this), |
5743 | xhr: function() { | |
5744 | var $xhr = $.ajaxSettings.xhr(); | |
5745 | if ($xhr) { | |
5746 | $xhr.upload.addEventListener('progress', function(event) { | |
32f6cd95 | 5747 | self._progress($uploadID, event); |
c1aaf7a7 MW |
5748 | }, false); |
5749 | } | |
5750 | return $xhr; | |
5751 | } | |
5752 | }); | |
5753 | } | |
5754 | }, | |
5755 | ||
5756 | /** | |
5757 | * Callback for success event | |
5758 | */ | |
32f6cd95 MW |
5759 | _success: function(uploadID, data) { |
5760 | console.debug(data); | |
c1aaf7a7 MW |
5761 | }, |
5762 | ||
5763 | /** | |
5764 | * Callback for error event | |
5765 | */ | |
5766 | _error: function(jqXHR, textStatus, errorThrown) { | |
5767 | console.debug(jqXHR.responseText); | |
5768 | }, | |
5769 | ||
5770 | /** | |
5771 | * Callback for progress event | |
5772 | */ | |
32f6cd95 | 5773 | _progress: function(uploadID, event) { |
c1aaf7a7 MW |
5774 | var $percentComplete = Math.round(event.loaded * 100 / event.total); |
5775 | ||
5776 | for (var $i = 0; $i < this._uploadMatrix[uploadID].length; $i++) { | |
5777 | this._uploadMatrix[uploadID][$i].find('progress').attr('value', $percentComplete); | |
5778 | } | |
5779 | }, | |
5780 | ||
32f6cd95 MW |
5781 | /** |
5782 | * Returns additional parameters. | |
5783 | */ | |
5784 | _getParameters: function() { | |
5785 | return {}; | |
5786 | }, | |
c1aaf7a7 MW |
5787 | |
5788 | _initFile: function(file) { | |
5789 | var $li = $('<li>'+file.name+' ('+file.size+')<progress max="100"></progress></li>'); | |
5790 | this._fileListSelector.append($li); | |
5791 | ||
5792 | return $li; | |
5793 | }, | |
5794 | ||
5795 | /** | |
5796 | * Shows the fallback overlay (work in progress) | |
5797 | */ | |
5798 | _showOverlay: function() { | |
5799 | var $self = this; | |
5800 | if (!this._overlay) { | |
5801 | // create overlay | |
c0bd9c8a | 5802 | this._overlay = $('<div style="display: none;"><form enctype="multipart/form-data" method="post" action="'+this._options.url+'"><dl><dt><label for="__fileUpload">File</label></dt><dd><input type="file" id="__fileUpload" name="'+this._name+'" '+(this._options.multiple ? 'multiple="true" ' : '')+'/></dd></dl><div class="formSubmit"><input type="submit" value="Upload" accesskey="s" /></div></form></div>'); |
c1aaf7a7 MW |
5803 | } |
5804 | ||
5805 | // create iframe | |
5806 | var $iframe = $('<iframe style="display: none"></iframe>'); // width: 300px; height: 100px; border: 5px solid red | |
5807 | $iframe.attr('name', $iframe.wcfIdentify()); | |
5808 | $('body').append($iframe); | |
5809 | this._overlay.find('form').attr('target', $iframe.wcfIdentify()); | |
5810 | ||
5811 | // add events (iframe onload) | |
5812 | $iframe.load(function() { | |
5813 | console.debug('iframe ready'); | |
5814 | console.debug($iframe.contents()); | |
5815 | }); | |
5816 | ||
5817 | this._overlay.find('form').submit(function() { | |
5818 | $iframe.data('loading', true); | |
5819 | $self._overlay.wcfDialog('close'); | |
5820 | }); | |
5821 | ||
5822 | this._overlay.wcfDialog({ | |
5823 | title: 'Upload', | |
5824 | onClose: function() { | |
5825 | if (!$iframe.data('loading')) { | |
5826 | $iframe.remove(); | |
5827 | } | |
5828 | } | |
5829 | }); | |
5830 | } | |
5831 | }); | |
5832 | ||
ee1a6ccb AE |
5833 | /** |
5834 | * Namespace for sortables. | |
5835 | */ | |
5836 | WCF.Sortable = {}; | |
5837 | ||
5838 | /** | |
5839 | * Sortable implementation for lists. | |
5840 | * | |
5841 | * @param string containerID | |
5842 | * @param string className | |
3926f3e3 AE |
5843 | * @param integer offset |
5844 | * @param object options | |
ee1a6ccb | 5845 | */ |
3926f3e3 | 5846 | WCF.Sortable.List = Class.extend({ |
2a16194a AE |
5847 | /** |
5848 | * additional parameters for AJAX request | |
5849 | * @var object | |
5850 | */ | |
5851 | _additionalParameters: { }, | |
5852 | ||
ee1a6ccb AE |
5853 | /** |
5854 | * action class name | |
5855 | * @var string | |
5856 | */ | |
5857 | _className: '', | |
5858 | ||
5859 | /** | |
5860 | * container id | |
5861 | * @var string | |
5862 | */ | |
5863 | _containerID: '', | |
5864 | ||
5865 | /** | |
5866 | * container object | |
5867 | * @var jQuery | |
5868 | */ | |
5869 | _container: null, | |
5870 | ||
5871 | /** | |
5872 | * notification object | |
5873 | * @var WCF.System.Notification | |
5874 | */ | |
5875 | _notification: null, | |
5876 | ||
b007ce67 AE |
5877 | /** |
5878 | * show order offset | |
5879 | * @var integer | |
5880 | */ | |
5881 | _offset: 0, | |
5882 | ||
076e8a3f AE |
5883 | /** |
5884 | * list of options | |
5885 | * @var object | |
5886 | */ | |
5887 | _options: { }, | |
5888 | ||
ee1a6ccb AE |
5889 | /** |
5890 | * proxy object | |
5891 | * @var WCF.Action.Proxy | |
5892 | */ | |
5893 | _proxy: null, | |
5894 | ||
5895 | /** | |
5896 | * object structure | |
5897 | * @var object | |
5898 | */ | |
5899 | _structure: { }, | |
5900 | ||
5901 | /** | |
5902 | * Creates a new sortable list. | |
5903 | * | |
5904 | * @param string containerID | |
5905 | * @param string className | |
b007ce67 | 5906 | * @param integer offset |
076e8a3f | 5907 | * @param object options |
afa0b934 | 5908 | * @param boolean isSimpleSorting |
2a16194a | 5909 | * @param object additionalParameters |
ee1a6ccb | 5910 | */ |
2a16194a AE |
5911 | init: function(containerID, className, offset, options, isSimpleSorting, additionalParameters) { |
5912 | this._additionalParameters = additionalParameters || { }; | |
ee1a6ccb AE |
5913 | this._containerID = $.wcfEscapeID(containerID); |
5914 | this._container = $('#' + this._containerID); | |
5915 | this._className = className; | |
b007ce67 | 5916 | this._offset = (offset) ? offset : 0; |
ee1a6ccb AE |
5917 | this._proxy = new WCF.Action.Proxy({ |
5918 | success: $.proxy(this._success, this) | |
5919 | }); | |
5920 | this._structure = { }; | |
5921 | ||
5922 | // init sortable | |
076e8a3f | 5923 | this._options = $.extend(true, { |
9dcf11e2 | 5924 | axis: 'y', |
afa0b934 | 5925 | connectWith: '#' + this._containerID + ' .sortableList', |
3d6b3f29 AE |
5926 | disableNesting: 'sortableNoNesting', |
5927 | errorClass: 'sortableInvalidTarget', | |
9dcf11e2 AE |
5928 | forcePlaceholderSize: true, |
5929 | helper: 'clone', | |
3d6b3f29 | 5930 | items: 'li:not(.sortableNoSorting)', |
9dcf11e2 | 5931 | opacity: .6, |
3d6b3f29 | 5932 | placeholder: 'sortablePlaceholder', |
9dcf11e2 AE |
5933 | tolerance: 'pointer', |
5934 | toleranceElement: '> span' | |
076e8a3f | 5935 | }, options || { }); |
afa0b934 AE |
5936 | |
5937 | if (isSimpleSorting) { | |
5938 | $('#' + this._containerID + ' .sortableList').sortable(this._options); | |
5939 | } | |
5940 | else { | |
5941 | $('#' + this._containerID + ' > .sortableList').wcfNestedSortable(this._options); | |
5942 | } | |
d1a6b448 | 5943 | |
6d8f21ac AE |
5944 | if (this._className) { |
5945 | this._container.find('.formSubmit > button[data-type="submit"]').click($.proxy(this._submit, this)); | |
5946 | } | |
ee1a6ccb AE |
5947 | }, |
5948 | ||
5949 | /** | |
5950 | * Saves object structure. | |
5951 | */ | |
d1a6b448 | 5952 | _submit: function() { |
2a16194a AE |
5953 | // reset structure |
5954 | this._structure = { }; | |
5955 | ||
ee1a6ccb | 5956 | // build structure |
3d6b3f29 | 5957 | this._container.find('.sortableList').each($.proxy(function(index, list) { |
ee1a6ccb AE |
5958 | var $list = $(list); |
5959 | var $parentID = $list.data('objectID'); | |
5960 | ||
83a29d77 AE |
5961 | if ($parentID !== undefined) { |
5962 | $list.children(this._options.items).each($.proxy(function(index, listItem) { | |
5963 | var $objectID = $(listItem).data('objectID'); | |
5964 | ||
5965 | if (!this._structure[$parentID]) { | |
5966 | this._structure[$parentID] = [ ]; | |
5967 | } | |
5968 | ||
5969 | this._structure[$parentID].push($objectID); | |
5970 | }, this)); | |
5971 | } | |
ee1a6ccb AE |
5972 | }, this)); |
5973 | ||
5974 | // send request | |
2a16194a | 5975 | var $parameters = $.extend(true, { |
ee1a6ccb | 5976 | data: { |
b007ce67 | 5977 | offset: this._offset, |
ee1a6ccb AE |
5978 | structure: this._structure |
5979 | } | |
2a16194a AE |
5980 | }, this._additionalParameters); |
5981 | ||
5982 | this._proxy.setOption('data', { | |
5983 | actionName: 'updatePosition', | |
5984 | className: this._className, | |
5985 | parameters: $parameters | |
ee1a6ccb AE |
5986 | }); |
5987 | this._proxy.sendRequest(); | |
5988 | }, | |
5989 | ||
5990 | /** | |
5991 | * Shows notification upon success. | |
5992 | * | |
5993 | * @param object data | |
5994 | * @param string textStatus | |
5995 | * @param jQuery jqXHR | |
5996 | */ | |
5997 | _success: function(data, textStatus, jqXHR) { | |
5998 | if (this._notification === null) { | |
79d0d1b4 | 5999 | this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.form.edit.success')); |
ee1a6ccb AE |
6000 | } |
6001 | ||
6002 | this._notification.show(); | |
6003 | } | |
3926f3e3 | 6004 | }); |
ee1a6ccb | 6005 | |
453aced6 AE |
6006 | WCF.Popover = Class.extend({ |
6007 | /** | |
6008 | * currently active element id | |
6009 | * @var string | |
6010 | */ | |
6011 | _activeElementID: '', | |
6012 | ||
2f326e85 AE |
6013 | /** |
6014 | * cancels popover | |
6015 | * @var boolean | |
6016 | */ | |
6017 | _cancelPopover: false, | |
6018 | ||
453aced6 AE |
6019 | /** |
6020 | * element data | |
6021 | * @var object | |
6022 | */ | |
6023 | _data: { }, | |
6024 | ||
6025 | /** | |
6026 | * default dimensions, should reflect the estimated size | |
6027 | * @var object | |
6028 | */ | |
6029 | _defaultDimensions: { | |
523908ab | 6030 | height: 150, |
b85dd964 | 6031 | width: 450 |
453aced6 AE |
6032 | }, |
6033 | ||
6034 | /** | |
6035 | * default orientation, may be a combintion of left/right and bottom/top | |
6036 | * @var object | |
6037 | */ | |
6038 | _defaultOrientation: { | |
6039 | x: 'right', | |
6040 | y: 'top' | |
6041 | }, | |
6042 | ||
6043 | /** | |
6044 | * delay to show or hide popover, values in miliseconds | |
6045 | * @var object | |
6046 | */ | |
6047 | _delay: { | |
6048 | show: 250, | |
6049 | hide: 500 | |
6050 | }, | |
6051 | ||
6052 | /** | |
6053 | * true, if an element is being hovered | |
6054 | * @var boolean | |
6055 | */ | |
6056 | _hoverElement: false, | |
6057 | ||
6058 | /** | |
6059 | * element id of element being hovered | |
6060 | * @var string | |
6061 | */ | |
6062 | _hoverElementID: '', | |
6063 | ||
6064 | /** | |
6065 | * true, if popover is being hovered | |
6066 | * @var boolean | |
6067 | */ | |
6068 | _hoverPopover: false, | |
6069 | ||
6070 | /** | |
6071 | * minimum margin (all directions) for popover | |
6072 | * @var integer | |
6073 | */ | |
6074 | _margin: 20, | |
6075 | ||
523908ab AE |
6076 | /** |
6077 | * periodical executer once element or popover is no longer being hovered | |
6078 | * @var WCF.PeriodicalExecuter | |
6079 | */ | |
6080 | _peOut: null, | |
6081 | ||
453aced6 AE |
6082 | /** |
6083 | * periodical executer once an element is being hovered | |
6084 | * @var WCF.PeriodicalExecuter | |
6085 | */ | |
6086 | _peOverElement: null, | |
6087 | ||
6088 | /** | |
6089 | * popover object | |
6090 | * @var jQuery | |
6091 | */ | |
6092 | _popover: null, | |
6093 | ||
5b755307 AE |
6094 | /** |
6095 | * popover content | |
6096 | * @var jQuery | |
6097 | */ | |
6098 | _popoverContent: null, | |
6099 | ||
6100 | ||
453aced6 AE |
6101 | /** |
6102 | * popover horizontal offset | |
6103 | * @var integer | |
6104 | */ | |
6105 | _popoverOffset: 10, | |
6106 | ||
6107 | /** | |
6108 | * element selector | |
6109 | * @var string | |
6110 | */ | |
6111 | _selector: '', | |
6112 | ||
6113 | /** | |
6114 | * Initializes a new WCF.Popover object. | |
6115 | * | |
6116 | * @param string selector | |
6117 | */ | |
6118 | init: function(selector) { | |
6119 | // assign default values | |
6120 | this._activeElementID = ''; | |
2f326e85 | 6121 | this._cancelPopover = false; |
453aced6 AE |
6122 | this._data = { }; |
6123 | this._defaultDimensions = { | |
523908ab AE |
6124 | height: 150, |
6125 | width: 450 | |
453aced6 AE |
6126 | }; |
6127 | this._defaultOrientation = { | |
6128 | x: 'right', | |
6129 | y: 'top' | |
6130 | }; | |
6131 | this._delay = { | |
6132 | show: 250, | |
6133 | hide: 500 | |
6134 | }; | |
6135 | this._hoverElement = false; | |
6136 | this._hoverElementID = ''; | |
6137 | this._hoverPopover = false; | |
6138 | this._margin = 20; | |
523908ab AE |
6139 | this._peOut = null; |
6140 | this._peOverElement = null; | |
453aced6 AE |
6141 | this._popoverOffset = 10; |
6142 | this._selector = selector; | |
6143 | ||
5b755307 AE |
6144 | this._popover = $('<div class="popover"><div class="popoverContent"></div></div>').hide().appendTo(document.body); |
6145 | this._popoverContent = this._popover.children('.popoverContent:eq(0)'); | |
523908ab | 6146 | this._popover.hover($.proxy(this._overPopover, this), $.proxy(this._out, this)); |
453aced6 AE |
6147 | |
6148 | this._initContainers(); | |
3c1f2137 | 6149 | WCF.DOMNodeInsertedHandler.addCallback('WCF.Popover.'+selector, $.proxy(this._initContainers, this)); |
453aced6 AE |
6150 | }, |
6151 | ||
6152 | /** | |
6153 | * Initializes all element triggers. | |
6154 | */ | |
6155 | _initContainers: function() { | |
6156 | var $elements = $(this._selector); | |
6157 | if (!$elements.length) { | |
6158 | return; | |
6159 | } | |
6160 | ||
6161 | $elements.each($.proxy(function(index, element) { | |
6162 | var $element = $(element); | |
6163 | var $elementID = $element.wcfIdentify(); | |
6164 | ||
6165 | if (!this._data[$elementID]) { | |
6166 | this._data[$elementID] = { | |
6167 | 'content': null, | |
6168 | 'isLoading': false | |
6169 | }; | |
6170 | ||
6171 | $element.hover($.proxy(this._overElement, this), $.proxy(this._out, this)); | |
2f326e85 | 6172 | |
57de5168 | 6173 | if ($element.getTagName() === 'a' && $element.attr('href')) { |
2f326e85 AE |
6174 | $element.click($.proxy(this._cancel, this)); |
6175 | } | |
453aced6 AE |
6176 | } |
6177 | }, this)); | |
6178 | }, | |
6179 | ||
2f326e85 AE |
6180 | /** |
6181 | * Cancels popovers if link is being clicked | |
6182 | */ | |
6183 | _cancel: function(event) { | |
6184 | this._cancelPopover = true; | |
6185 | this._hide(true); | |
6186 | }, | |
6187 | ||
453aced6 AE |
6188 | /** |
6189 | * Triggered once an element is being hovered. | |
6190 | * | |
6191 | * @param object event | |
6192 | */ | |
6193 | _overElement: function(event) { | |
2f326e85 AE |
6194 | if (this._cancelPopover) { |
6195 | return; | |
6196 | } | |
6197 | ||
453aced6 AE |
6198 | if (this._peOverElement !== null) { |
6199 | this._peOverElement.stop(); | |
6200 | } | |
6201 | ||
6202 | var $elementID = $(event.currentTarget).wcfIdentify(); | |
6203 | this._hoverElementID = $elementID; | |
6204 | this._peOverElement = new WCF.PeriodicalExecuter($.proxy(function(pe) { | |
6205 | pe.stop(); | |
6206 | ||
6207 | // still above the same element | |
6208 | if (this._hoverElementID === $elementID) { | |
6209 | this._activeElementID = $elementID; | |
6210 | this._prepare(); | |
6211 | } | |
6212 | }, this), this._delay.show); | |
6213 | ||
6214 | this._hoverElement = true; | |
6215 | this._hoverPopover = false; | |
6216 | }, | |
6217 | ||
6218 | /** | |
6219 | * Prepares popover to be displayed. | |
6220 | */ | |
6221 | _prepare: function() { | |
2f326e85 AE |
6222 | if (this._cancelPopover) { |
6223 | return; | |
6224 | } | |
6225 | ||
523908ab AE |
6226 | if (this._peOut !== null) { |
6227 | this._peOut.stop(); | |
6228 | } | |
6229 | ||
453aced6 AE |
6230 | // hide and reset |
6231 | if (this._popover.is(':visible')) { | |
5b755307 | 6232 | this._hide(true); |
453aced6 AE |
6233 | } |
6234 | ||
6235 | // insert html | |
6236 | if (!this._data[this._activeElementID].loading && this._data[this._activeElementID].content) { | |
389d57cd AE |
6237 | WCF.DOMNodeInsertedHandler.enable(); |
6238 | ||
5b755307 | 6239 | this._popoverContent.html(this._data[this._activeElementID].content); |
389d57cd AE |
6240 | |
6241 | WCF.DOMNodeInsertedHandler.disable(); | |
453aced6 AE |
6242 | } |
6243 | else { | |
453aced6 AE |
6244 | this._data[this._activeElementID].loading = true; |
6245 | } | |
6246 | ||
6247 | // get dimensions | |
6248 | var $dimensions = this._popover.show().getDimensions(); | |
6249 | if (this._data[this._activeElementID].loading) { | |
6250 | $dimensions = { | |
6251 | height: Math.max($dimensions.height, this._defaultDimensions.height), | |
6252 | width: Math.max($dimensions.width, this._defaultDimensions.width) | |
6253 | }; | |
6254 | } | |
523908ab AE |
6255 | else { |
6256 | $dimensions = this._fixElementDimensions(this._popover, $dimensions); | |
6257 | } | |
453aced6 AE |
6258 | this._popover.hide(); |
6259 | ||
6260 | // get orientation | |
6261 | var $orientation = this._getOrientation($dimensions.height, $dimensions.width); | |
6262 | this._popover.css(this._getCSS($orientation.x, $orientation.y)); | |
6263 | ||
379272f7 AE |
6264 | // apply orientation to popover |
6265 | this._popover.removeClass('bottom left right top').addClass($orientation.x).addClass($orientation.y); | |
6266 | ||
453aced6 AE |
6267 | this._show(); |
6268 | }, | |
6269 | ||
6270 | /** | |
6271 | * Displays the popover. | |
6272 | */ | |
6273 | _show: function() { | |
2f326e85 AE |
6274 | if (this._cancelPopover) { |
6275 | return; | |
6276 | } | |
6277 | ||
57de5168 | 6278 | this._popover.stop().show().css({ opacity: 1 }).wcfFadeIn(); |
453aced6 | 6279 | |
1e5b5b23 AE |
6280 | if (this._data[this._activeElementID].loading) { |
6281 | this._loadContent(); | |
6282 | } | |
5b755307 AE |
6283 | else { |
6284 | this._popoverContent.css({ opacity: 1 }); | |
6285 | } | |
453aced6 AE |
6286 | }, |
6287 | ||
6288 | /** | |
6289 | * Loads content, should be overwritten by child classes. | |
6290 | */ | |
6291 | _loadContent: function() { }, | |
6292 | ||
379272f7 AE |
6293 | /** |
6294 | * Inserts content and animating transition. | |
6295 | * | |
6296 | * @param string elementID | |
6297 | * @param boolean animate | |
6298 | */ | |
6299 | _insertContent: function(elementID, content, animate) { | |
6300 | this._data[elementID] = { | |
6301 | content: content, | |
6302 | loading: false | |
6303 | }; | |
6304 | ||
6305 | // only update content if element id is active | |
6306 | if (this._activeElementID === elementID) { | |
603cfb39 AE |
6307 | WCF.DOMNodeInsertedHandler.enable(); |
6308 | ||
1e5b5b23 AE |
6309 | if (animate) { |
6310 | // get current dimensions | |
5b755307 | 6311 | var $dimensions = this._popoverContent.getDimensions(); |
1e5b5b23 AE |
6312 | |
6313 | // insert new content | |
603cfb39 AE |
6314 | this._popoverContent.css({ |
6315 | height: 'auto', | |
6316 | width: 'auto' | |
6317 | }); | |
5b755307 AE |
6318 | this._popoverContent.html(this._data[elementID].content); |
6319 | var $newDimensions = this._popoverContent.getDimensions(); | |
1e5b5b23 AE |
6320 | |
6321 | // enforce current dimensions and remove HTML | |
5b755307 | 6322 | this._popoverContent.html('').css({ |
1e5b5b23 AE |
6323 | height: $dimensions.height + 'px', |
6324 | width: $dimensions.width + 'px' | |
6325 | }); | |
6326 | ||
6327 | // animate to new dimensons | |
5b755307 AE |
6328 | var self = this; |
6329 | this._popoverContent.animate({ | |
1e5b5b23 AE |
6330 | height: $newDimensions.height + 'px', |
6331 | width: $newDimensions.width + 'px' | |
6332 | }, 300, function() { | |
603cfb39 AE |
6333 | WCF.DOMNodeInsertedHandler.enable(); |
6334 | ||
5b755307 | 6335 | self._popoverContent.html(self._data[elementID].content).css({ opacity: 0 }).animate({ opacity: 1 }, 200); |
603cfb39 AE |
6336 | |
6337 | WCF.DOMNodeInsertedHandler.disable(); | |
1e5b5b23 AE |
6338 | }); |
6339 | } | |
6340 | else { | |
6341 | // insert new content | |
5b755307 | 6342 | this._popoverContent.html(this._data[elementID].content); |
1e5b5b23 | 6343 | } |
603cfb39 AE |
6344 | |
6345 | WCF.DOMNodeInsertedHandler.disable(); | |
379272f7 AE |
6346 | } |
6347 | }, | |
6348 | ||
453aced6 AE |
6349 | /** |
6350 | * Hides the popover. | |
6351 | */ | |
5b755307 AE |
6352 | _hide: function(disableAnimation) { |
6353 | var self = this; | |
6354 | this._popoverContent.stop(); | |
6355 | this._popover.stop(); | |
6356 | ||
6357 | if (disableAnimation) { | |
57de5168 | 6358 | self._popover.css({ opacity: 0 }).hide(); |
5b755307 AE |
6359 | self._popoverContent.empty().css({ height: 'auto', opacity: 0, width: 'auto' }); |
6360 | } | |
6361 | else { | |
6362 | this._popover.wcfFadeOut(function() { | |
fc821303 AE |
6363 | self._popoverContent.empty().css({ height: 'auto', opacity: 0, width: 'auto' }); |
6364 | self._popover.hide(); | |
5b755307 AE |
6365 | }); |
6366 | } | |
453aced6 AE |
6367 | }, |
6368 | ||
6369 | /** | |
6370 | * Triggered once popover is being hovered. | |
6371 | */ | |
6372 | _overPopover: function() { | |
523908ab AE |
6373 | if (this._peOut !== null) { |
6374 | this._peOut.stop(); | |
6375 | } | |
6376 | ||
453aced6 AE |
6377 | this._hoverElement = false; |
6378 | this._hoverPopover = true; | |
6379 | }, | |
6380 | ||
6381 | /** | |
6382 | * Triggered once element *or* popover is now longer hovered. | |
6383 | */ | |
6384 | _out: function(event) { | |
2f326e85 AE |
6385 | if (this._cancelPopover) { |
6386 | return; | |
6387 | } | |
6388 | ||
1e5b5b23 | 6389 | this._hoverElementID = ''; |
453aced6 AE |
6390 | this._hoverElement = false; |
6391 | this._hoverPopover = false; | |
6392 | ||
523908ab | 6393 | this._peOut = new WCF.PeriodicalExecuter($.proxy(function(pe) { |
453aced6 AE |
6394 | pe.stop(); |
6395 | ||
6396 | // hide popover is neither element nor popover was hovered given time | |
6397 | if (!this._hoverElement && !this._hoverPopover) { | |
5b755307 | 6398 | this._hide(false); |
453aced6 AE |
6399 | } |
6400 | }, this), this._delay.hide); | |
6401 | }, | |
6402 | ||
6403 | /** | |
6404 | * Resolves popover orientation, tries to use default orientation first. | |
6405 | * | |
6406 | * @param integer height | |
6407 | * @param integer width | |
6408 | * @return object | |
6409 | */ | |
6410 | _getOrientation: function(height, width) { | |
6411 | // get offsets and dimensions | |
6412 | var $element = $('#' + this._activeElementID); | |
523908ab | 6413 | var $offsets = $element.getOffsets('offset'); |
453aced6 AE |
6414 | var $elementDimensions = $element.getDimensions(); |
6415 | var $documentDimensions = $(document).getDimensions(); | |
6416 | ||
6417 | // try default orientation first | |
6418 | var $orientationX = (this._defaultOrientation.x === 'left') ? 'left' : 'right'; | |
6419 | var $orientationY = (this._defaultOrientation.y === 'bottom') ? 'bottom' : 'top'; | |
379272f7 | 6420 | var $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width); |
453aced6 AE |
6421 | |
6422 | if ($result.flawed) { | |
6423 | // try flipping orientationX | |
6424 | $orientationX = ($orientationX === 'left') ? 'right' : 'left'; | |
6425 | $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width); | |
6426 | ||
6427 | if ($result.flawed) { | |
6428 | // try flipping orientationY while maintaing original orientationX | |
6429 | $orientationX = ($orientationX === 'right') ? 'left' : 'right'; | |
6430 | $orientationY = ($orientationY === 'bottom') ? 'top' : 'bottom'; | |
6431 | $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width); | |
6432 | ||
6433 | if ($result.flawed) { | |
6434 | // try flipping both orientationX and orientationY compared to default values | |
6435 | $orientationX = ($orientationX === 'left') ? 'right' : 'left'; | |
6436 | $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width); | |
6437 | ||
6438 | if ($result.flawed) { | |
6439 | // fuck this shit, we will use the default orientation | |
6440 | $orientationX = (this._defaultOrientationX === 'left') ? 'left' : 'right'; | |
6441 | $orientationY = (this._defaultOrientationY === 'bottom') ? 'bottom' : 'top'; | |
6442 | } | |
6443 | } | |
6444 | } | |
6445 | } | |
6446 | ||
6447 | return { | |
6448 | x: $orientationX, | |
6449 | y: $orientationY | |
d27e2667 | 6450 | }; |
453aced6 AE |
6451 | }, |
6452 | ||
6453 | /** | |
6454 | * Evaluates if popover fits into given orientation. | |
6455 | * | |
6456 | * @param string orientationX | |
6457 | * @param string orientationY | |
6458 | * @param object offsets | |
6459 | * @param object elementDimensions | |
6460 | * @param object documentDimensions | |
6461 | * @param integer height | |
6462 | * @param integer width | |
6463 | * @return object | |
6464 | */ | |
6465 | _evaluateOrientation: function(orientationX, orientationY, offsets, elementDimensions, documentDimensions, height, width) { | |
6466 | var $heightDifference = 0, $widthDifference = 0; | |
6467 | switch (orientationX) { | |
6468 | case 'left': | |
6469 | $widthDifference = offsets.left - width; | |
6470 | break; | |
6471 | ||
6472 | case 'right': | |
523908ab | 6473 | $widthDifference = documentDimensions.width - (offsets.left + width); |
453aced6 AE |
6474 | break; |
6475 | } | |
6476 | ||
6477 | switch (orientationY) { | |
6478 | case 'bottom': | |
523908ab | 6479 | $heightDifference = documentDimensions.height - (offsets.top + elementDimensions.height + this._popoverOffset + height); |
453aced6 AE |
6480 | break; |
6481 | ||
6482 | case 'top': | |
6483 | $heightDifference = offsets.top - (height - this._popoverOffset); | |
6484 | break; | |
6485 | } | |
6486 | ||
6487 | // check if both difference are above margin | |
6488 | var $flawed = false; | |
6489 | if ($heightDifference < this._margin || $widthDifference < this._margin) { | |
6490 | $flawed = true; | |
6491 | } | |
6492 | ||
6493 | return { | |
6494 | flawed: $flawed, | |
6495 | x: $widthDifference, | |
6496 | y: $heightDifference | |
6497 | }; | |
6498 | }, | |
6499 | ||
6500 | /** | |
6501 | * Computes CSS for popover. | |
6502 | * | |
6503 | * @param string orientationX | |
6504 | * @param string orientationY | |
6505 | * @return object | |
6506 | */ | |
6507 | _getCSS: function(orientationX, orientationY) { | |
6508 | var $css = { | |
6509 | bottom: 'auto', | |
6510 | left: 'auto', | |
453aced6 AE |
6511 | right: 'auto', |
6512 | top: 'auto' | |
6513 | }; | |
6514 | ||
6515 | var $element = $('#' + this._activeElementID); | |
523908ab AE |
6516 | var $offsets = $element.getOffsets('offset'); |
6517 | var $elementDimensions = this._fixElementDimensions($element, $element.getDimensions()); | |
379272f7 | 6518 | var $windowDimensions = $(window).getDimensions(); |
453aced6 AE |
6519 | |
6520 | switch (orientationX) { | |
6521 | case 'left': | |
379272f7 | 6522 | $css.right = $windowDimensions.width - ($offsets.left + $elementDimensions.width); |
453aced6 AE |
6523 | break; |
6524 | ||
6525 | case 'right': | |
6526 | $css.left = $offsets.left; | |
6527 | break; | |
6528 | } | |
6529 | ||
6530 | switch (orientationY) { | |
6531 | case 'bottom': | |
6532 | $css.top = $offsets.top + ($elementDimensions.height + this._popoverOffset); | |
6533 | break; | |
6534 | ||
6535 | case 'top': | |
379272f7 | 6536 | $css.bottom = $windowDimensions.height - ($offsets.top - this._popoverOffset); |
453aced6 AE |
6537 | break; |
6538 | } | |
6539 | ||
6540 | return $css; | |
523908ab AE |
6541 | }, |
6542 | ||
6543 | /** | |
6544 | * Tries to fix dimensions if element is partially hidden (overflow: hidden). | |
6545 | * | |
6546 | * @param jQuery element | |
6547 | * @param object dimensions | |
6548 | * @return dimensions | |
6549 | */ | |
6550 | _fixElementDimensions: function(element, dimensions) { | |
6551 | var $parentDimensions = element.parent().getDimensions(); | |
6552 | ||
6553 | if ($parentDimensions.height < dimensions.height) { | |
6554 | dimensions.height = $parentDimensions.height; | |
6555 | } | |
6556 | ||
6557 | if ($parentDimensions.width < dimensions.width) { | |
6558 | dimensions.width = $parentDimensions.width; | |
6559 | } | |
6560 | ||
6561 | return dimensions; | |
453aced6 AE |
6562 | } |
6563 | }); | |
6564 | ||
cede0aec AE |
6565 | /** |
6566 | * Provides an extensible item list with built-in search. | |
6567 | * | |
6568 | * @param string itemListSelector | |
6569 | * @param string searchInputSelector | |
6570 | */ | |
d27e2667 | 6571 | WCF.EditableItemList = Class.extend({ |
cede0aec AE |
6572 | /** |
6573 | * allows custom input not recognized by search to be added | |
6574 | * @var boolean | |
6575 | */ | |
6576 | _allowCustomInput: false, | |
6577 | ||
6578 | /** | |
6579 | * action class name | |
6580 | * @var string | |
6581 | */ | |
6582 | _className: '', | |
6583 | ||
6584 | /** | |
6585 | * internal data storage | |
6586 | * @var mixed | |
6587 | */ | |
d27e2667 | 6588 | _data: { }, |
cede0aec AE |
6589 | |
6590 | /** | |
6591 | * form container | |
6592 | * @var jQuery | |
6593 | */ | |
6594 | _form: null, | |
6595 | ||
6596 | /** | |
6597 | * item list container | |
6598 | * @var jQuery | |
6599 | */ | |
d27e2667 | 6600 | _itemList: null, |
cede0aec AE |
6601 | |
6602 | /** | |
6603 | * current object id | |
6604 | * @var integer | |
6605 | */ | |
6606 | _objectID: 0, | |
6607 | ||
6608 | /** | |
6609 | * object type id | |
6610 | * @var integer | |
6611 | */ | |
6612 | _objectTypeID: 0, | |
6613 | ||
6614 | /** | |
6615 | * search controller | |
6616 | * @var WCF.Search.Base | |
6617 | */ | |
d27e2667 | 6618 | _search: null, |
cede0aec AE |
6619 | |
6620 | /** | |
6621 | * search input element | |
6622 | * @var jQuery | |
6623 | */ | |
d27e2667 AE |
6624 | _searchInput: null, |
6625 | ||
cede0aec AE |
6626 | /** |
6627 | * Creates a new WCF.EditableItemList object. | |
6628 | * | |
6629 | * @param string itemListSelector | |
6630 | * @param string searchInputSelector | |
6631 | */ | |
d27e2667 AE |
6632 | init: function(itemListSelector, searchInputSelector) { |
6633 | this._itemList = $(itemListSelector); | |
6634 | this._searchInput = $(searchInputSelector); | |
6635 | ||
6636 | if (!this._itemList.length || !this._searchInput.length) { | |
6637 | console.debug("[WCF.EditableItemList] Item list and/or search input do not exist, aborting."); | |
6638 | return; | |
6639 | } | |
6640 | ||
cede0aec AE |
6641 | this._objectID = this._getObjectID(); |
6642 | this._objectTypeID = this._getObjectTypeID(); | |
6643 | ||
d27e2667 AE |
6644 | // bind item listener |
6645 | this._itemList.find('.jsEditableItem').click($.proxy(this._click, this)); | |
cede0aec AE |
6646 | |
6647 | // create item list | |
6648 | if (!this._itemList.children('ul').length) { | |
6649 | $('<ul />').appendTo(this._itemList); | |
6650 | } | |
6651 | this._itemList = this._itemList.children('ul'); | |
6652 | ||
6653 | // bind form submit | |
6654 | this._form = this._itemList.parents('form').submit($.proxy(this._submit, this)); | |
6655 | ||
6656 | if (this._allowCustomInput) { | |
80da9de3 AE |
6657 | this._searchInput.keydown($.proxy(this._keyDown, this)); |
6658 | } | |
6659 | }, | |
6660 | ||
6661 | /** | |
6662 | * Handles the key down event. | |
6663 | * | |
6664 | * @param object event | |
6665 | */ | |
6666 | _keyDown: function(event) { | |
6667 | if (event === null || (event.which === 13 || event.which === 188)) { | |
6668 | var $value = $.trim(this._searchInput.val()); | |
6669 | if ($value === '') { | |
cede0aec | 6670 | return true; |
80da9de3 AE |
6671 | } |
6672 | ||
6673 | this.addItem({ | |
6674 | objectID: 0, | |
6675 | label: $value | |
cede0aec | 6676 | }); |
80da9de3 AE |
6677 | |
6678 | // reset input | |
6679 | this._searchInput.val(''); | |
6680 | ||
6681 | if (event !== null) { | |
6682 | event.stopPropagation(); | |
6683 | } | |
6684 | ||
6685 | return false; | |
cede0aec | 6686 | } |
80da9de3 AE |
6687 | |
6688 | return true; | |
d27e2667 AE |
6689 | }, |
6690 | ||
6691 | /** | |
6692 | * Loads raw data and converts it into internal structure. Override this methods | |
6693 | * in your derived classes. | |
6694 | * | |
6695 | * @param object data | |
6696 | */ | |
6697 | load: function(data) { }, | |
6698 | ||
cede0aec AE |
6699 | /** |
6700 | * Removes an item on click. | |
6701 | * | |
6702 | * @param object event | |
6703 | * @return boolean | |
6704 | */ | |
d27e2667 AE |
6705 | _click: function(event) { |
6706 | var $element = $(event.currentTarget); | |
6707 | var $objectID = $element.data('objectID'); | |
cede0aec | 6708 | var $label = $element.data('label'); |
d27e2667 | 6709 | |
0b9520f4 MS |
6710 | if (this._search) { |
6711 | this._search.removeExcludedSearchValue($label); | |
6712 | } | |
cede0aec | 6713 | this._removeItem($objectID, $label); |
d27e2667 AE |
6714 | |
6715 | $element.remove(); | |
6716 | ||
6717 | event.stopPropagation(); | |
6718 | return false; | |
6719 | }, | |
6720 | ||
cede0aec AE |
6721 | /** |
6722 | * Returns current object id. | |
6723 | * | |
6724 | * @return integer | |
6725 | */ | |
6726 | _getObjectID: function() { | |
6727 | return 0; | |
6728 | }, | |
6729 | ||
6730 | /** | |
6731 | * Returns current object type id. | |
6732 | * | |
6733 | * @return integer | |
6734 | */ | |
6735 | _getObjectTypeID: function() { | |
6736 | return 0; | |
6737 | }, | |
6738 | ||
6739 | /** | |
6740 | * Adds a new item to the list. | |
6741 | * | |
6742 | * @param object data | |
6743 | * @return boolean | |
6744 | */ | |
6745 | addItem: function(data) { | |
6746 | if (this._data[data.objectID]) { | |
8717050f AE |
6747 | if (!(data.objectID === 0 && this._allowCustomInput)) { |
6748 | return true; | |
6749 | } | |
cede0aec AE |
6750 | } |
6751 | ||
6752 | var $listItem = $('<li class="badge">' + data.label + '</li>').data('objectID', data.objectID).data('label', data.label).appendTo(this._itemList); | |
6753 | $listItem.click($.proxy(this._click, this)); | |
d27e2667 | 6754 | |
0b9520f4 MS |
6755 | if (this._search) { |
6756 | this._search.addExcludedSearchValue(data.label); | |
6757 | } | |
cede0aec AE |
6758 | this._addItem(data.objectID, data.label); |
6759 | ||
6760 | return true; | |
6761 | }, | |
6762 | ||
6763 | /** | |
6764 | * Handles form submit, override in your class. | |
6765 | */ | |
80da9de3 AE |
6766 | _submit: function() { |
6767 | this._keyDown(null); | |
6768 | }, | |
cede0aec AE |
6769 | |
6770 | /** | |
6771 | * Adds an item to internal storage. | |
6772 | * | |
6773 | * @param integer objectID | |
6774 | * @param string label | |
6775 | */ | |
6776 | _addItem: function(objectID, label) { | |
6777 | this._data[objectID] = label; | |
6778 | }, | |
6779 | ||
6780 | /** | |
6781 | * Removes an item from internal storage. | |
6782 | * | |
6783 | * @param integer objectID | |
6784 | * @param string label | |
6785 | */ | |
6786 | _removeItem: function(objectID, label) { | |
6787 | delete this._data[objectID]; | |
d27e2667 AE |
6788 | } |
6789 | }); | |
6790 | ||
27c3b95f AE |
6791 | /** |
6792 | * Provides a generic sitemap. | |
6793 | */ | |
6794 | WCF.Sitemap = Class.extend({ | |
6795 | /** | |
6796 | * sitemap name cache | |
9f959ced | 6797 | * @var array |
27c3b95f AE |
6798 | */ |
6799 | _cache: [ ], | |
6800 | ||
6801 | /** | |
6802 | * dialog overlay | |
6803 | * @var jQuery | |
6804 | */ | |
6805 | _dialog: null, | |
6806 | ||
6807 | /** | |
6808 | * initialization state | |
6809 | * @var boolean | |
6810 | */ | |
6811 | _didInit: false, | |
6812 | ||
6813 | /** | |
6814 | * action proxy | |
6815 | * @var WCF.Action.Proxy | |
6816 | */ | |
6817 | _proxy: null, | |
6818 | ||
6819 | /** | |
6820 | * Initializes the generic sitemap. | |
6821 | */ | |
6822 | init: function() { | |
6823 | $('#sitemap').click($.proxy(this._click, this)); | |
6824 | ||
6825 | this._cache = [ ]; | |
6826 | this._dialog = null; | |
6827 | this._didInit = false; | |
6828 | this._proxy = new WCF.Action.Proxy({ | |
6829 | success: $.proxy(this._success, this) | |
6830 | }); | |
6831 | }, | |
6832 | ||
6833 | /** | |
6834 | * Handles clicks on the sitemap icon. | |
6835 | */ | |
6836 | _click: function() { | |
6837 | if (this._dialog === null) { | |
6838 | this._dialog = $('<div id="sitemapDialog" />').appendTo(document.body); | |
6839 | ||
6840 | this._proxy.setOption('data', { | |
6841 | actionName: 'getSitemap', | |
6842 | className: 'wcf\\data\\sitemap\\SitemapAction' | |
6843 | }); | |
6844 | this._proxy.sendRequest(); | |
6845 | } | |
6846 | else { | |
6847 | this._dialog.wcfDialog('open'); | |
6848 | } | |
6849 | }, | |
6850 | ||
6851 | /** | |
6852 | * Handles successful AJAX responses. | |
6853 | * | |
6854 | * @param object data | |
6855 | * @param string textStatus | |
6856 | * @param jQuery jqXHR | |
6857 | */ | |
6858 | _success: function(data, textStatus, jqXHR) { | |
6859 | if (this._didInit) { | |
6860 | this._cache.push(data.returnValues.sitemapName); | |
6861 | ||
47f83a32 | 6862 | this._dialog.find('#sitemap_' + data.returnValues.sitemapName).html(data.returnValues.template); |
27c3b95f AE |
6863 | |
6864 | // redraw dialog | |
6865 | this._dialog.wcfDialog('render'); | |
6866 | } | |
6867 | else { | |
6868 | // mark sitemap name as loaded | |
6869 | this._cache.push(data.returnValues.sitemapName); | |
6870 | ||
6871 | // insert sitemap template | |
6872 | this._dialog.html(data.returnValues.template); | |
6873 | ||
6874 | // bind event listener | |
6875 | this._dialog.find('.sitemapNavigation').click($.proxy(this._navigate, this)); | |
6876 | ||
6877 | // show dialog | |
6878 | this._dialog.wcfDialog({ | |
6879 | title: WCF.Language.get('wcf.sitemap.title') | |
6880 | }); | |
6881 | ||
6882 | this._didInit = true; | |
6883 | } | |
6884 | }, | |
6885 | ||
6886 | /** | |
6887 | * Navigates between different sitemaps. | |
6888 | * | |
6889 | * @param object event | |
6890 | */ | |
6891 | _navigate: function(event) { | |
6892 | var $sitemapName = $(event.currentTarget).data('sitemapName'); | |
6893 | if (WCF.inArray($sitemapName, this._cache)) { | |
47f83a32 | 6894 | this._dialog.find('.tabMenuContainer').wcfTabs('select', 'sitemap_' + $sitemapName); |
27c3b95f AE |
6895 | |
6896 | // redraw dialog | |
6897 | this._dialog.wcfDialog('render'); | |
6898 | } | |
6899 | else { | |
6900 | this._proxy.setOption('data', { | |
6901 | actionName: 'getSitemap', | |
6902 | className: 'wcf\\data\\sitemap\\SitemapAction', | |
6903 | parameters: { | |
6904 | sitemapName: $sitemapName | |
6905 | } | |
6906 | }); | |
6907 | this._proxy.sendRequest(); | |
6908 | } | |
6909 | } | |
6910 | }); | |
6911 | ||
a5423278 AE |
6912 | /** |
6913 | * Provides a language chooser. | |
6914 | * | |
6915 | * @param string containerID | |
6916 | * @param string inputFieldID | |
6917 | * @param integer languageID | |
6918 | * @param object languages | |
6919 | * @param object callback | |
6920 | */ | |
6921 | WCF.Language.Chooser = Class.extend({ | |
6922 | /** | |
6923 | * callback object | |
6924 | * @var object | |
6925 | */ | |
6926 | _callback: null, | |
6927 | ||
6928 | /** | |
6929 | * dropdown object | |
6930 | * @var jQuery | |
6931 | */ | |
6932 | _dropdown: null, | |
6933 | ||
6934 | /** | |
6935 | * input field | |
6936 | * @var jQuery | |
6937 | */ | |
6938 | _input: null, | |
6939 | ||
6940 | /** | |
6941 | * Initializes the language chooser. | |
6942 | * | |
6943 | * @param string containerID | |
6944 | * @param string inputFieldID | |
6945 | * @param integer languageID | |
6946 | * @param object languages | |
6947 | * @param object callback | |
65b1d6a0 | 6948 | * @param boolean allowEmptyValue |
a5423278 | 6949 | */ |
65b1d6a0 | 6950 | init: function(containerID, inputFieldID, languageID, languages, callback, allowEmptyValue) { |
a5423278 AE |
6951 | var $container = $('#' + containerID); |
6952 | if ($container.length != 1) { | |
6953 | console.debug("[WCF.Language.Chooser] Invalid container id '" + containerID + "' given"); | |
6954 | return; | |
6955 | } | |
6956 | ||
6957 | // bind language id input | |
6958 | this._input = $('#' + inputFieldID); | |
6959 | if (!this._input.length) { | |
6960 | this._input = $('<input type="hidden" name="' + inputFieldID + '" value="' + languageID + '" />').appendTo($container); | |
6961 | } | |
6962 | ||
6963 | // handle callback | |
6964 | if (callback !== undefined) { | |
6965 | if (!$.isFunction(callback)) { | |
6966 | console.debug("[WCF.Language.Chooser] Given callback is invalid"); | |
6967 | return; | |
6968 | } | |
6969 | ||
6970 | this._callback = callback; | |
6971 | } | |
6972 | ||
6973 | // create language dropdown | |
6974 | this._dropdown = $('<div class="dropdown" id="' + containerID + '-languageChooser" />').appendTo($container); | |
f3e301ca | 6975 | $('<div class="dropdownToggle boxFlag box24" data-toggle="' + containerID + '-languageChooser"></div>').appendTo(this._dropdown); |
a5423278 AE |
6976 | var $dropdownMenu = $('<ul class="dropdownMenu" />').appendTo(this._dropdown); |
6977 | ||
6978 | for (var $languageID in languages) { | |
6979 | var $language = languages[$languageID]; | |
f3e301ca | 6980 | var $item = $('<li class="boxFlag"><a class="box24"><div class="framed"><img src="' + $language.iconPath + '" alt="" class="iconFlag" /></div> <hgroup><h1>' + $language.languageName + '</h1></hgroup></a></li>').appendTo($dropdownMenu); |
a5423278 AE |
6981 | $item.data('languageID', $languageID).click($.proxy(this._click, this)); |
6982 | ||
6983 | // update dropdown label | |
6984 | if ($languageID == languageID) { | |
f3e301ca MW |
6985 | var $html = $('' + $item.html()); |
6986 | var $innerContent = $html.children().detach(); | |
6987 | this._dropdown.children('.dropdownToggle').empty().append($innerContent); | |
a5423278 AE |
6988 | } |
6989 | } | |
6990 | ||
65b1d6a0 AE |
6991 | // allow an empty selection (e.g. using as language filter) |
6992 | if (allowEmptyValue) { | |
6993 | $('<li class="dropdownDivider" />').appendTo($dropdownMenu); | |
6994 | var $item = $('<li><a>' + WCF.Language.get('wcf.global.language.noSelection') + '</a></li>').data('languageID', 0).click($.proxy(this._click, this)).appendTo($dropdownMenu); | |
6995 | ||
6996 | if (languageID === 0) { | |
6997 | this._dropdown.children('.dropdownToggle').empty().append($item.html()); | |
6998 | } | |
6999 | } | |
7000 | ||
a5423278 AE |
7001 | WCF.Dropdown.init(); |
7002 | }, | |
7003 | ||
7004 | /** | |
7005 | * Handles click events. | |
7006 | * | |
7007 | * @param object event | |
7008 | */ | |
7009 | _click: function(event) { | |
7010 | var $item = $(event.currentTarget); | |
65b1d6a0 | 7011 | var $languageID = $item.data('languageID'); |
a5423278 AE |
7012 | |
7013 | // update input field | |
65b1d6a0 | 7014 | this._input.val($languageID); |
a5423278 AE |
7015 | |
7016 | // update dropdown label | |
f3e301ca | 7017 | var $html = $('' + $item.html()); |
65b1d6a0 | 7018 | var $innerContent = ($languageID === 0) ? $html : $html.children().detach(); |
f3e301ca | 7019 | this._dropdown.children('.dropdownToggle').empty().append($innerContent); |
a5423278 AE |
7020 | |
7021 | // execute callback | |
7022 | if (this._callback !== null) { | |
7023 | this._callback($item); | |
7024 | } | |
7025 | } | |
7026 | }); | |
7027 | ||
83736ee3 AE |
7028 | /** |
7029 | * Namespace for style related classes. | |
7030 | */ | |
7031 | WCF.Style = { }; | |
7032 | ||
7033 | /** | |
7034 | * Provides a visual style chooser. | |
7035 | */ | |
7036 | WCF.Style.Chooser = Class.extend({ | |
7037 | /** | |
7038 | * dialog overlay | |
7039 | * @var jQuery | |
7040 | */ | |
7041 | _dialog: null, | |
7042 | ||
7043 | /** | |
7044 | * action proxy | |
7045 | * @var WCF.Action.Proxy | |
7046 | */ | |
7047 | _proxy: null, | |
7048 | ||
7049 | /** | |
7050 | * Initializes the style chooser class. | |
7051 | */ | |
7052 | init: function() { | |
c275a0f3 | 7053 | $('<li class="styleChooser"><a>' + WCF.Language.get('wcf.style.changeStyle') + '</a></li>').appendTo($('#footerNavigation > ul.navigationItems')).click($.proxy(this._showDialog, this)); |
83736ee3 AE |
7054 | |
7055 | this._proxy = new WCF.Action.Proxy({ | |
7056 | success: $.proxy(this._success, this) | |
7057 | }); | |
7058 | }, | |
7059 | ||
7060 | /** | |
7061 | * Displays the style chooser dialog. | |
7062 | */ | |
7063 | _showDialog: function() { | |
7064 | if (this._dialog === null) { | |
7065 | this._dialog = $('<div id="styleChooser" />').hide().appendTo(document.body); | |
7066 | this._loadDialog(); | |
7067 | } | |
7068 | else { | |
7069 | this._dialog.wcfDialog({ | |
7070 | title: WCF.Language.get('wcf.style.changeStyle') | |
7071 | }); | |
7072 | } | |
7073 | }, | |
7074 | ||
7075 | /** | |
7076 | * Loads the style chooser dialog. | |
7077 | */ | |
7078 | _loadDialog: function() { | |
7079 | this._proxy.setOption('data', { | |
7080 | actionName: 'getStyleChooser', | |
7081 | className: 'wcf\\data\\style\\StyleAction' | |
7082 | }); | |
7083 | this._proxy.sendRequest(); | |
7084 | }, | |
7085 | ||
7086 | /** | |
7087 | * Handles successful AJAX requests. | |
7088 | * | |
7089 | * @param object data | |
7090 | * @param string textStatus | |
7091 | * @param jQuery jqXHR | |
7092 | */ | |
7093 | _success: function(data, textStatus, jqXHR) { | |
7094 | if (data.returnValues.actionName === 'changeStyle') { | |
7095 | window.location.reload(); | |
7096 | return; | |
7097 | } | |
7098 | ||
7099 | this._dialog.html(data.returnValues.template); | |
7100 | this._dialog.find('li').addClass('pointer').click($.proxy(this._click, this)); | |
7101 | ||
7102 | this._showDialog(); | |
7103 | }, | |
7104 | ||
7105 | /** | |
7106 | * Changes user style. | |
7107 | * | |
7108 | * @param object event | |
7109 | */ | |
7110 | _click: function(event) { | |
7111 | this._proxy.setOption('data', { | |
7112 | actionName: 'changeStyle', | |
7113 | className: 'wcf\\data\\style\\StyleAction', | |
7114 | objectIDs: [ $(event.currentTarget).data('styleID') ] | |
7115 | }); | |
7116 | this._proxy.sendRequest(); | |
7117 | } | |
7118 | }); | |
7119 | ||
271dbf28 AE |
7120 | /** |
7121 | * Converts static user panel items into interactive dropdowns. | |
7122 | * | |
7123 | * @param string containerID | |
7124 | */ | |
7125 | WCF.UserPanel = Class.extend({ | |
7126 | /** | |
7127 | * target container | |
7128 | * @var jQuery | |
7129 | */ | |
7130 | _container: null, | |
7131 | ||
7132 | /** | |
7133 | * initialization state | |
7134 | * @var boolean | |
7135 | */ | |
7136 | _didLoad: false, | |
7137 | ||
7138 | /** | |
7139 | * original link element | |
7140 | * @var jQuery | |
7141 | */ | |
7142 | _link: null, | |
7143 | ||
7144 | /** | |
7145 | * reverts to original link if return values are empty | |
7146 | * @var boolean | |
7147 | */ | |
7148 | _revertOnEmpty: true, | |
7149 | ||
7150 | /** | |
7151 | * Initialites the WCF.UserPanel class. | |
7152 | * | |
7153 | * @param string containerID | |
7154 | */ | |
7155 | init: function(containerID) { | |
7156 | this._container = $('#' + containerID); | |
7157 | this._didLoad = false; | |
7158 | this._revertOnEmpty = true; | |
7159 | ||
7160 | if (this._container.length != 1) { | |
7161 | console.debug("[WCF.UserPanel] Unable to find container identfied by '" + containerID + "', aborting."); | |
7162 | return; | |
7163 | } | |
7164 | ||
7165 | if (this._container.data('count')) { | |
cd1c77bd | 7166 | WCF.DOMNodeInsertedHandler.enable(); |
271dbf28 | 7167 | this._convert(); |
cd1c77bd | 7168 | WCF.DOMNodeInsertedHandler.disable(); |
271dbf28 AE |
7169 | } |
7170 | }, | |
7171 | ||
7172 | /** | |
7173 | * Converts link into an interactive dropdown menu. | |
7174 | */ | |
7175 | _convert: function() { | |
271dbf28 AE |
7176 | this._container.addClass('dropdown'); |
7177 | this._link = this._container.children('a').remove(); | |
7178 | ||
7179 | $('<a class="dropdownToggle jsTooltip" title="' + this._container.data('title') + '">' + this._link.html() + '</a>').appendTo(this._container).click($.proxy(this._click, this)); | |
7180 | var $dropdownMenu = $('<ul class="dropdownMenu" />').appendTo(this._container); | |
7181 | $('<li class="jsDropdownPlaceholder"><span>' + WCF.Language.get('wcf.global.loading') + '</span></li>').appendTo($dropdownMenu); | |
7182 | ||
7183 | this._addDefaultItems($dropdownMenu); | |
271dbf28 AE |
7184 | }, |
7185 | ||
7186 | /** | |
7187 | * Adds default items to dropdown menu. | |
7188 | * | |
7189 | * @param jQuery dropdownMenu | |
7190 | */ | |
7191 | _addDefaultItems: function(dropdownMenu) { }, | |
7192 | ||
7193 | /** | |
7194 | * Adds a dropdown divider. | |
7195 | * | |
7196 | * @param jQuery dropdownMenu | |
7197 | */ | |
7198 | _addDivider: function(dropdownMenu) { | |
7199 | $('<li class="dropdownDivider" />').appendTo(dropdownMenu); | |
7200 | }, | |
7201 | ||
7202 | /** | |
7203 | * Handles clicks on the dropdown item. | |
7204 | */ | |
7205 | _click: function() { | |
7206 | if (this._didLoad) { | |
7207 | return; | |
7208 | } | |
7209 | ||
7210 | new WCF.Action.Proxy({ | |
7211 | autoSend: true, | |
7212 | data: this._getParameters(), | |
7213 | success: $.proxy(this._success, this) | |
7214 | }); | |
7215 | ||
7216 | this._didLoad = true; | |
7217 | }, | |
7218 | ||
7219 | /** | |
7220 | * Returns a list of parameters for AJAX request. | |
7221 | * | |
7222 | * @return object | |
7223 | */ | |
7224 | _getParameters: function() { | |
7225 | return { }; | |
7226 | }, | |
7227 | ||
7228 | /** | |
7229 | * Handles successful AJAX requests. | |
7230 | * | |
7231 | * @param object data | |
7232 | * @param string textStatus | |
7233 | * @param jQuery jqXHR | |
7234 | */ | |
7235 | _success: function(data, textStatus, jqXHR) { | |
7236 | if (data.returnValues && data.returnValues.template) { | |
7237 | var $dropdownMenu = this._container.children('.dropdownMenu'); | |
7238 | $dropdownMenu.children('.jsDropdownPlaceholder').remove(); | |
7239 | $('' + data.returnValues.template).prependTo($dropdownMenu); | |
7240 | } | |
7241 | else { | |
7242 | this._container.removeClass('dropdown').empty(); | |
7243 | this._link.appendTo(this._container); | |
7244 | ||
7245 | // remove badge | |
7246 | this._container.find('.badge').remove(); | |
7247 | } | |
7248 | } | |
7249 | }); | |
7250 | ||
9dcf11e2 AE |
7251 | /** |
7252 | * WCF implementation for nested sortables. | |
7253 | */ | |
174ce795 | 7254 | $.widget("ui.wcfNestedSortable", $.extend({}, $.mjs.nestedSortable.prototype, { |
9dcf11e2 AE |
7255 | _clearEmpty: function(item) { |
7256 | /* Does nothing because we want to keep empty lists */ | |
7257 | } | |
7258 | })); | |
7259 | ||
25763f41 AE |
7260 | /** |
7261 | * WCF implementation for dialogs, based upon ideas by jQuery UI. | |
7262 | */ | |
7263 | $.widget('ui.wcfDialog', { | |
7264 | /** | |
7265 | * close button | |
7266 | * @var jQuery | |
7267 | */ | |
7268 | _closeButton: null, | |
9f959ced | 7269 | |
25763f41 AE |
7270 | /** |
7271 | * dialog container | |
7272 | * @var jQuery | |
7273 | */ | |
7274 | _container: null, | |
9f959ced | 7275 | |
25763f41 AE |
7276 | /** |
7277 | * dialog content | |
7278 | * @var jQuery | |
7279 | */ | |
7280 | _content: null, | |
9f959ced | 7281 | |
1e7d6513 AE |
7282 | /** |
7283 | * dialog content dimensions | |
7284 | * @var object | |
7285 | */ | |
7286 | _contentDimensions: null, | |
9f959ced | 7287 | |
9acd5123 AE |
7288 | /** |
7289 | * rendering state | |
7290 | * @var boolean | |
7291 | */ | |
7292 | _isRendering: false, | |
9f959ced | 7293 | |
25763f41 AE |
7294 | /** |
7295 | * modal overlay | |
7296 | * @var jQuery | |
7297 | */ | |
7298 | _overlay: null, | |
9f959ced | 7299 | |
25763f41 AE |
7300 | /** |
7301 | * plain html for title | |
7302 | * @var string | |
7303 | */ | |
7304 | _title: null, | |
9f959ced | 7305 | |
25763f41 AE |
7306 | /** |
7307 | * title bar | |
7308 | * @var jQuery | |
7309 | */ | |
7310 | _titlebar: null, | |
9f959ced | 7311 | |
25763f41 AE |
7312 | /** |
7313 | * dialog visibility state | |
7314 | * @var boolean | |
7315 | */ | |
7316 | _isOpen: false, | |
9f959ced | 7317 | |
25763f41 AE |
7318 | /** |
7319 | * option list | |
7320 | * @var object | |
7321 | */ | |
7322 | options: { | |
7323 | // dialog | |
7324 | autoOpen: true, | |
7325 | closable: true, | |
7326 | closeButtonLabel: null, | |
7327 | hideTitle: false, | |
7328 | modal: true, | |
7329 | title: '', | |
184a8d6d | 7330 | zIndex: 400, |
9f959ced | 7331 | |
25763f41 AE |
7332 | // AJAX support |
7333 | ajax: false, | |
7334 | data: { }, | |
324e8301 | 7335 | showLoadingOverlay: true, |
25763f41 AE |
7336 | success: null, |
7337 | type: 'POST', | |
34e158d9 AE |
7338 | url: 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN + SID_ARG_2ND, |
7339 | ||
7340 | // event callbacks | |
7341 | onClose: null, | |
7342 | onShow: null | |
25763f41 | 7343 | }, |
9f959ced | 7344 | |
25763f41 AE |
7345 | /** |
7346 | * Initializes a new dialog. | |
7347 | */ | |
7348 | _init: function() { | |
25763f41 AE |
7349 | if (this.options.ajax) { |
7350 | new WCF.Action.Proxy({ | |
7351 | autoSend: true, | |
7352 | data: this.options.data, | |
324e8301 | 7353 | showLoadingOverlay: this.options.showLoadingOverlay, |
25763f41 AE |
7354 | success: $.proxy(this._success, this), |
7355 | type: this.options.type, | |
7356 | url: this.options.url | |
7357 | }); | |
9f959ced | 7358 | |
25763f41 AE |
7359 | // force open if using AJAX |
7360 | this.options.autoOpen = true; | |
9f959ced | 7361 | |
feda5d4b AE |
7362 | // apply loading overlay |
7363 | this._content.addClass('overlayLoading'); | |
25763f41 | 7364 | } |
9f959ced | 7365 | |
25763f41 AE |
7366 | if (this.options.autoOpen) { |
7367 | this.open(); | |
7368 | } | |
9f959ced | 7369 | |
25763f41 | 7370 | // act on resize |
6e3d3bf1 | 7371 | $(window).resize($.proxy(this._resize, this)); |
25763f41 | 7372 | }, |
9f959ced | 7373 | |
25763f41 AE |
7374 | /** |
7375 | * Creates a new dialog instance. | |
7376 | */ | |
7377 | _create: function() { | |
4445e78f AE |
7378 | if (this.options.closeButtonLabel === null) { |
7379 | this.options.closeButtonLabel = WCF.Language.get('wcf.global.button.close'); | |
7380 | } | |
7381 | ||
7382 | WCF.DOMNodeInsertedHandler.enable(); | |
7383 | ||
25763f41 | 7384 | // create dialog container |
f74219d0 MK |
7385 | this._container = $('<div class="dialogContainer" />').hide().css({ zIndex: this.options.zIndex }).appendTo(document.body); |
7386 | this._titlebar = $('<header class="dialogTitlebar" />').hide().appendTo(this._container); | |
7387 | this._title = $('<span class="dialogTitle" />').hide().appendTo(this._titlebar); | |
4445e78f | 7388 | this._closeButton = $('<a class="dialogCloseButton jsTooltip" title="' + this.options.closeButtonLabel + '"><span /></a>').click($.proxy(this.close, this)).hide().appendTo(this._titlebar); |
f74219d0 | 7389 | this._content = $('<div class="dialogContent" />').appendTo(this._container); |
25763f41 | 7390 | |
f74219d0 MK |
7391 | this._setOption('title', this.options.title); |
7392 | this._setOption('closable', this.options.closable); | |
9f959ced | 7393 | |
25763f41 | 7394 | // move target element into content |
6e3d3bf1 | 7395 | var $content = this.element.detach(); |
25763f41 | 7396 | this._content.html($content); |
9f959ced | 7397 | |
25763f41 AE |
7398 | // create modal view |
7399 | if (this.options.modal) { | |
5a1b4042 AE |
7400 | this._overlay = $('#jsWcfDialogOverlay'); |
7401 | if (!this._overlay.length) { | |
f74219d0 | 7402 | this._overlay = $('<div id="jsWcfDialogOverlay" class="dialogOverlay" />').css({ height: '100%', zIndex: 399 }).appendTo(document.body); |
5a1b4042 AE |
7403 | } |
7404 | ||
25763f41 AE |
7405 | if (this.options.closable) { |
7406 | this._overlay.click($.proxy(this.close, this)); | |
7407 | ||
7408 | $(document).keyup($.proxy(function(event) { | |
7409 | if (event.keyCode && event.keyCode === $.ui.keyCode.ESCAPE) { | |
7410 | this.close(); | |
7411 | event.preventDefault(); | |
7412 | } | |
7413 | }, this)); | |
7414 | } | |
7415 | } | |
4445e78f AE |
7416 | |
7417 | WCF.DOMNodeInsertedHandler.disable(); | |
25763f41 | 7418 | }, |
ef097134 | 7419 | |
f74219d0 MK |
7420 | /** |
7421 | * Sets the given option to the given value. | |
7422 | * See the jQuery UI widget documentation for more. | |
7423 | */ | |
7424 | _setOption: function(key, value) { | |
7425 | this.options[key] = value; | |
7426 | ||
7427 | if (key == 'hideTitle' || key == 'title') { | |
7428 | if (!this.options.hideTitle && this.options.title != '') { | |
7429 | this._title.html(this.options.title).show(); | |
7430 | } else { | |
7431 | this._title.html(''); | |
7432 | } | |
7433 | } else if (key == 'closable' || key == 'closeButtonLabel') { | |
7434 | if (this.options.closable) { | |
49b2955d AE |
7435 | WCF.DOMNodeInsertedHandler.enable(); |
7436 | ||
f74219d0 | 7437 | this._closeButton.attr('title', this.options.closeButtonLabel).show().find('span').html(this.options.closeButtonLabel); |
49b2955d AE |
7438 | |
7439 | WCF.DOMNodeInsertedHandler.disable(); | |
f74219d0 MK |
7440 | } else { |
7441 | this._closeButton.hide(); | |
7442 | } | |
7443 | } | |
7444 | ||
7445 | if ((!this.options.hideTitle && this.options.title != '') || this.options.closable) { | |
7446 | this._titlebar.show(); | |
7447 | } else { | |
7448 | this._titlebar.hide(); | |
7449 | } | |
7450 | ||
7451 | return this; | |
7452 | }, | |
7453 | ||
25763f41 AE |
7454 | /** |
7455 | * Handles successful AJAX requests. | |
7456 | * | |
7457 | * @param object data | |
7458 | * @param string textStatus | |
7459 | * @param jQuery jqXHR | |
7460 | */ | |
7461 | _success: function(data, textStatus, jqXHR) { | |
94d5ff20 MK |
7462 | if (this._isOpen) { |
7463 | // initialize dialog content | |
7464 | this._initDialog(data); | |
7465 | ||
7466 | // remove loading overlay | |
7467 | this._content.removeClass('overlayLoading'); | |
7468 | ||
7469 | if (this.options.success !== null && $.isFunction(this.options.success)) { | |
7470 | this.options.success(data, textStatus, jqXHR); | |
7471 | } | |
25763f41 AE |
7472 | } |
7473 | }, | |
ef097134 | 7474 | |
1e7d6513 AE |
7475 | /** |
7476 | * Initializes dialog content if applicable. | |
7477 | * | |
7478 | * @param object data | |
7479 | */ | |
7480 | _initDialog: function(data) { | |
7481 | // insert template | |
f74219d0 MK |
7482 | if (this._getResponseValue(data, 'template')) { |
7483 | this._content.children().html(this._getResponseValue(data, 'template')); | |
1e7d6513 AE |
7484 | this.render(); |
7485 | } | |
f74219d0 MK |
7486 | |
7487 | // set title | |
7488 | if (this._getResponseValue(data, 'title')) { | |
7489 | this._setOption('title', this._getResponseValue(data, 'title')); | |
7490 | } | |
1e7d6513 | 7491 | }, |
9f959ced | 7492 | |
1e7d6513 AE |
7493 | /** |
7494 | * Returns a response value, taking care of different object | |
7495 | * structure returned by AJAXProxy. | |
7496 | * | |
7497 | * @param object data | |
7498 | * @param string key | |
7499 | */ | |
7500 | _getResponseValue: function(data, key) { | |
7501 | if (data.returnValues && data.returnValues[key]) { | |
7502 | return data.returnValues[key]; | |
7503 | } | |
7504 | else if (data[key]) { | |
7505 | return data[key]; | |
7506 | } | |
9f959ced | 7507 | |
1e7d6513 AE |
7508 | return null; |
7509 | }, | |
9f959ced | 7510 | |
25763f41 AE |
7511 | /** |
7512 | * Opens this dialog. | |
7513 | */ | |
7514 | open: function() { | |
7515 | if (this.isOpen()) { | |
7516 | return; | |
7517 | } | |
9f959ced | 7518 | |
1e7d6513 | 7519 | if (this._overlay !== null) { |
5a1b4042 AE |
7520 | WCF.activeDialogs++; |
7521 | ||
7522 | if (WCF.activeDialogs === 1) { | |
7523 | this._overlay.show(); | |
7524 | } | |
1e7d6513 | 7525 | } |
9f959ced | 7526 | |
25763f41 AE |
7527 | this.render(); |
7528 | this._isOpen = true; | |
7529 | }, | |
9f959ced | 7530 | |
25763f41 AE |
7531 | /** |
7532 | * Returns true, if dialog is visible. | |
7533 | * | |
7534 | * @return boolean | |
7535 | */ | |
7536 | isOpen: function() { | |
7537 | return this._isOpen; | |
7538 | }, | |
ef097134 | 7539 | |
25763f41 AE |
7540 | /** |
7541 | * Closes this dialog. | |
7542 | */ | |
7543 | close: function() { | |
3eaee49b | 7544 | if (!this.isOpen() || !this.options.closable) { |
25763f41 AE |
7545 | return; |
7546 | } | |
9f959ced | 7547 | |
25763f41 AE |
7548 | this._isOpen = false; |
7549 | this._container.wcfFadeOut(); | |
9f959ced | 7550 | |
25763f41 | 7551 | if (this._overlay !== null) { |
5a1b4042 AE |
7552 | WCF.activeDialogs--; |
7553 | ||
7554 | if (WCF.activeDialogs === 0) { | |
7555 | this._overlay.hide(); | |
7556 | } | |
25763f41 | 7557 | } |
34e158d9 AE |
7558 | |
7559 | if (this.options.onClose !== null) { | |
7560 | this.options.onClose(); | |
7561 | } | |
25763f41 | 7562 | }, |
ef097134 | 7563 | |
6e3d3bf1 AE |
7564 | /** |
7565 | * Renders dialog on resize if visible. | |
7566 | */ | |
7567 | _resize: function() { | |
7568 | if (this.isOpen()) { | |
7569 | this.render(); | |
7570 | } | |
7571 | }, | |
7572 | ||
25763f41 AE |
7573 | /** |
7574 | * Renders this dialog, should be called whenever content is updated. | |
7575 | */ | |
7576 | render: function() { | |
7577 | if (!this.isOpen()) { | |
7578 | // temporarily display container | |
7579 | this._container.show(); | |
7580 | } | |
1e7d6513 AE |
7581 | else { |
7582 | // remove fixed content dimensions for calculation | |
7583 | this._content.css({ | |
7584 | height: 'auto', | |
7585 | width: 'auto' | |
7586 | }); | |
7587 | } | |
9f959ced | 7588 | |
abe2607e AE |
7589 | // force content to be visible |
7590 | this._content.children().each(function() { | |
7591 | $(this).show(); | |
7592 | }); | |
9f959ced | 7593 | |
9acd5123 AE |
7594 | // handle multiple rendering requests |
7595 | if (this._isRendering) { | |
7596 | // stop current process | |
7597 | this._container.stop(); | |
7598 | this._content.stop(); | |
2bee924f AE |
7599 | |
7600 | // set dialog to be fully opaque, should prevent weird bugs in WebKit | |
7601 | this._container.show().css('opacity', 1.0); | |
9acd5123 | 7602 | } |
9f959ced | 7603 | |
a41af14a MW |
7604 | if (this._content.find('.formSubmit').length) { |
7605 | this._content.addClass('dialogForm'); | |
7606 | } | |
7607 | else { | |
7608 | this._content.removeClass('dialogForm'); | |
7609 | } | |
7610 | ||
25763f41 AE |
7611 | // calculate dimensions |
7612 | var $windowDimensions = $(window).getDimensions(); | |
7613 | var $containerDimensions = this._container.getDimensions('outer'); | |
d7d9f722 | 7614 | var $contentDimensions = this._content.getDimensions(); |
9f959ced | 7615 | |
abe2607e AE |
7616 | // calculate maximum content height |
7617 | var $heightDifference = $containerDimensions.height - $contentDimensions.height; | |
49b2955d | 7618 | var $maximumHeight = $windowDimensions.height - $heightDifference - 120; |
abe2607e AE |
7619 | this._content.css({ maxHeight: $maximumHeight + 'px' }); |
7620 | ||
7621 | // re-caculate values if container height was previously limited | |
7622 | if ($maximumHeight < $contentDimensions.height) { | |
7623 | $containerDimensions = this._container.getDimensions('outer'); | |
7624 | } | |
9f959ced | 7625 | |
9acd5123 AE |
7626 | // handle multiple rendering requests |
7627 | if (this._isRendering) { | |
7628 | // use current dimensions as previous ones | |
7629 | this._contentDimensions = this._getContentDimensions($maximumHeight); | |
7630 | } | |
9f959ced | 7631 | |
9acd5123 AE |
7632 | // calculate new dimensions |
7633 | $contentDimensions = this._getContentDimensions($maximumHeight); | |
9f959ced | 7634 | |
25763f41 AE |
7635 | // move container |
7636 | var $leftOffset = Math.round(($windowDimensions.width - $containerDimensions.width) / 2); | |
7637 | var $topOffset = Math.round(($windowDimensions.height - $containerDimensions.height) / 2); | |
9f959ced | 7638 | |
25763f41 AE |
7639 | // place container at 20% height if possible |
7640 | var $desiredTopOffset = Math.round(($windowDimensions.height / 100) * 20); | |
7641 | if ($desiredTopOffset < $topOffset) { | |
7642 | $topOffset = $desiredTopOffset; | |
7643 | } | |
9f959ced | 7644 | |
25763f41 AE |
7645 | if (!this.isOpen()) { |
7646 | // hide container again | |
7647 | this._container.hide(); | |
9f959ced | 7648 | |
25763f41 AE |
7649 | // apply offset |
7650 | this._container.css({ | |
7651 | left: $leftOffset + 'px', | |
7652 | top: $topOffset + 'px' | |
7653 | }); | |
9f959ced | 7654 | |
1e7d6513 AE |
7655 | // save current dimensions |
7656 | this._contentDimensions = $contentDimensions; | |
9f959ced | 7657 | |
1e7d6513 AE |
7658 | // force dimensions |
7659 | this._content.css({ | |
7660 | height: this._contentDimensions.height + 'px', | |
7661 | width: this._contentDimensions.width + 'px' | |
7662 | }); | |
9f959ced | 7663 | |
25763f41 | 7664 | // fade in container |
9acd5123 AE |
7665 | this._container.wcfFadeIn($.proxy(function() { |
7666 | this._isRendering = false; | |
7667 | })); | |
25763f41 AE |
7668 | } |
7669 | else { | |
1e7d6513 AE |
7670 | // save reference (used in callback) |
7671 | var $content = this._content; | |
1af4ce06 | 7672 | |
1e7d6513 AE |
7673 | // force previous dimensions |
7674 | $content.css({ | |
7675 | height: this._contentDimensions.height + 'px', | |
7676 | width: this._contentDimensions.width + 'px' | |
7677 | }); | |
9f959ced | 7678 | |
1e7d6513 AE |
7679 | // apply new dimensions |
7680 | $content.animate({ | |
1af4ce06 AE |
7681 | height: ($contentDimensions.height) + 'px', |
7682 | width: ($contentDimensions.width) + 'px' | |
79537c05 | 7683 | }, 300, function() { |
1e7d6513 AE |
7684 | // remove static dimensions |
7685 | $content.css({ | |
7686 | height: 'auto', | |
7687 | width: 'auto' | |
7688 | }); | |
7689 | }); | |
7690 | ||
7691 | // store new dimensions | |
7692 | this._contentDimensions = $contentDimensions; | |
9f959ced | 7693 | |
1e7d6513 | 7694 | // move container |
79537c05 | 7695 | this._isRendering = true; |
25763f41 AE |
7696 | this._container.animate({ |
7697 | left: $leftOffset + 'px', | |
7698 | top: $topOffset + 'px' | |
79537c05 | 7699 | }, 300, $.proxy(function() { |
9acd5123 | 7700 | this._isRendering = false; |
79537c05 | 7701 | }, this)); |
9acd5123 | 7702 | } |
34e158d9 AE |
7703 | |
7704 | if (this.options.onShow !== null) { | |
7705 | this.options.onShow(); | |
7706 | } | |
9acd5123 | 7707 | }, |
9f959ced | 7708 | |
9acd5123 AE |
7709 | /** |
7710 | * Returns calculated content dimensions. | |
7711 | * | |
7712 | * @param integer maximumHeight | |
7713 | * @return object | |
7714 | */ | |
7715 | _getContentDimensions: function(maximumHeight) { | |
7716 | var $contentDimensions = this._content.getDimensions(); | |
9f959ced | 7717 | |
9acd5123 AE |
7718 | // set height to maximum height if exceeded |
7719 | if ($contentDimensions.height > maximumHeight) { | |
7720 | $contentDimensions.height = maximumHeight; | |
25763f41 | 7721 | } |
9f959ced | 7722 | |
9acd5123 | 7723 | return $contentDimensions; |
25763f41 AE |
7724 | } |
7725 | }); | |
7726 | ||
158bd3ca | 7727 | /** |
18404c0a | 7728 | * Custom tab menu implementation for WCF. |
158bd3ca TD |
7729 | */ |
7730 | $.widget('ui.wcfTabs', $.ui.tabs, { | |
18404c0a AE |
7731 | /** |
7732 | * Workaround for ids containing a dot ".", until jQuery UI devs learn | |
7733 | * to properly escape ids ... (it took 18 months until they finally | |
7734 | * fixed it!) | |
7735 | * | |
7736 | * @see http://bugs.jqueryui.com/ticket/4681 | |
7737 | * @see $.ui.tabs.prototype._sanitizeSelector() | |
7738 | */ | |
158bd3ca TD |
7739 | _sanitizeSelector: function(hash) { |
7740 | return hash.replace(/([:\.])/g, '\\$1'); | |
18404c0a | 7741 | }, |
f24f0823 | 7742 | |
18404c0a AE |
7743 | /** |
7744 | * @see $.ui.tabs.prototype.select() | |
7745 | */ | |
7746 | select: function(index) { | |
7747 | if (!$.isNumeric(index)) { | |
7748 | // panel identifier given | |
7749 | this.panels.each(function(i, panel) { | |
7750 | if ($(panel).wcfIdentify() === index) { | |
7751 | index = i; | |
7752 | return false; | |
7753 | } | |
7754 | }); | |
7755 | ||
7756 | // unable to identify panel | |
7757 | if (!$.isNumeric(index)) { | |
7758 | console.debug("[ui.wcfTabs] Unable to find panel identified by '" + index + "', aborting."); | |
7759 | return; | |
7760 | } | |
7761 | } | |
dbd319de | 7762 | |
18404c0a AE |
7763 | $.ui.tabs.prototype.select.apply(this, arguments); |
7764 | }, | |
9f959ced | 7765 | |
18404c0a AE |
7766 | /** |
7767 | * Returns the currently selected tab index. | |
7768 | * | |
7769 | * @return integer | |
7770 | */ | |
7771 | getCurrentIndex: function() { | |
f4126129 | 7772 | return this.lis.index(this.lis.filter('.ui-tabs-selected')); |
f24f0823 AE |
7773 | }, |
7774 | ||
7775 | /** | |
7776 | * Returns true, if identifier is used by an anchor. | |
7777 | * | |
7778 | * @param string identifier | |
7779 | * @param boolean isChildren | |
7780 | * @return boolean | |
7781 | */ | |
7782 | hasAnchor: function(identifier, isChildren) { | |
7783 | var $matches = false; | |
7784 | ||
7785 | this.anchors.each(function(index, anchor) { | |
7786 | var $href = $(anchor).attr('href'); | |
7787 | if (/#.+/.test($href)) { | |
7788 | // split by anchor | |
7789 | var $parts = $href.split('#', 2); | |
7790 | if (isChildren) { | |
7791 | $parts = $parts[1].split('-', 2); | |
7792 | } | |
7793 | ||
7794 | if ($parts[1] === identifier) { | |
7795 | $matches = true; | |
7796 | ||
7797 | // terminate loop | |
7798 | return false; | |
7799 | } | |
7800 | } | |
7801 | }); | |
7802 | ||
7803 | return $matches; | |
3d57a2dd AE |
7804 | }, |
7805 | ||
7806 | /** | |
7807 | * Shows default tab. | |
7808 | */ | |
7809 | revertToDefault: function() { | |
7810 | var $active = this.element.data('active'); | |
7811 | if (!$active || $active === '') $active = 0; | |
7812 | ||
7813 | this.select($active); | |
158bd3ca TD |
7814 | } |
7815 | }); | |
7816 | ||
7817 | /** | |
7818 | * jQuery widget implementation of the wcf pagination. | |
7819 | */ | |
7820 | $.widget('ui.wcfPages', { | |
7821 | SHOW_LINKS: 11, | |
7822 | SHOW_SUB_LINKS: 20, | |
7823 | ||
7824 | options: { | |
7825 | // vars | |
7826 | activePage: 1, | |
7827 | maxPage: 1, | |
7828 | ||
7829 | // icons | |
90f2f032 | 7830 | previousIcon: null, |
90f2f032 TD |
7831 | arrowDownIcon: null, |
7832 | nextIcon: null, | |
158bd3ca TD |
7833 | |
7834 | // language | |
7835 | // we use options here instead of language variables, because the paginator is not only usable with pages | |
7836 | nextPage: null, | |
9c1e5045 | 7837 | previousPage: null |
158bd3ca TD |
7838 | }, |
7839 | ||
7840 | /** | |
7841 | * Creates the pages widget. | |
7842 | */ | |
7843 | _create: function() { | |
7844 | if (this.options.nextPage === null) this.options.nextPage = WCF.Language.get('wcf.global.page.next'); | |
7845 | if (this.options.previousPage === null) this.options.previousPage = WCF.Language.get('wcf.global.page.previous'); | |
0c15085d MS |
7846 | if (this.options.previousIcon === null) this.options.previousIcon = WCF.Icon.get('wcf.icon.circleArrowLeft'); |
7847 | if (this.options.nextIcon === null) this.options.nextIcon = WCF.Icon.get('wcf.icon.circleArrowRight'); | |
7848 | if (this.options.arrowDownIcon === null) this.options.arrowDownIcon = WCF.Icon.get('wcf.icon.arrowDown'); | |
158bd3ca | 7849 | |
b9698c4c | 7850 | this.element.addClass('pageNavigation'); |
158bd3ca TD |
7851 | |
7852 | this._render(); | |
7853 | }, | |
7854 | ||
7855 | /** | |
7856 | * Destroys the pages widget. | |
7857 | */ | |
7858 | destroy: function() { | |
7859 | $.Widget.prototype.destroy.apply(this, arguments); | |
7860 | ||
7861 | this.element.children().remove(); | |
7862 | }, | |
7863 | ||
7864 | /** | |
ebbf5629 | 7865 | * Renders the pages widget. |
158bd3ca TD |
7866 | */ |
7867 | _render: function() { | |
7868 | // only render if we have more than 1 page | |
7869 | if (!this.options.disabled && this.options.maxPage > 1) { | |
3bdc6920 AE |
7870 | var $hasHiddenPages = false; |
7871 | ||
158bd3ca TD |
7872 | // make sure pagination is visible |
7873 | if (this.element.hasClass('hidden')) { | |
7874 | this.element.removeClass('hidden'); | |
7875 | } | |
7876 | this.element.show(); | |
7877 | ||
7878 | this.element.children().remove(); | |
7879 | ||
7880 | var $pageList = $('<ul></ul>'); | |
7881 | this.element.append($pageList); | |
7882 | ||
b9698c4c | 7883 | var $previousElement = $('<li></li>').addClass('button skip'); |
158bd3ca TD |
7884 | $pageList.append($previousElement); |
7885 | ||
7886 | if (this.options.activePage > 1) { | |
7887 | var $previousLink = $('<a' + ((this.options.previousPage != null) ? (' title="' + this.options.previousPage + '"') : ('')) + '></a>'); | |
7888 | $previousElement.append($previousLink); | |
7889 | this._bindSwitchPage($previousLink, this.options.activePage - 1); | |
7890 | ||
7891 | var $previousImage = $('<img src="' + this.options.previousIcon + '" alt="" />'); | |
7892 | $previousLink.append($previousImage); | |
7893 | } | |
7894 | else { | |
5814a7f5 | 7895 | var $previousImage = $('<img src="' + this.options.previousIcon + '" alt="" />'); |
158bd3ca | 7896 | $previousElement.append($previousImage); |
be21c4eb | 7897 | $previousElement.addClass('disabled'); |
5814a7f5 | 7898 | $previousImage.addClass('disabled'); |
158bd3ca | 7899 | } |
5814a7f5 | 7900 | $previousImage.addClass('icon16'); |
158bd3ca TD |
7901 | |
7902 | // add first page | |
7903 | $pageList.append(this._renderLink(1)); | |
7904 | ||
7905 | // calculate page links | |
7906 | var $maxLinks = this.SHOW_LINKS - 4; | |
7907 | var $linksBefore = this.options.activePage - 2; | |
7908 | if ($linksBefore < 0) $linksBefore = 0; | |
7909 | var $linksAfter = this.options.maxPage - (this.options.activePage + 1); | |
7910 | if ($linksAfter < 0) $linksAfter = 0; | |
7911 | if (this.options.activePage > 1 && this.options.activePage < this.options.maxPage) $maxLinks--; | |
7912 | ||
7913 | var $half = $maxLinks / 2; | |
7914 | var $left = this.options.activePage; | |
7915 | var $right = this.options.activePage; | |
7916 | if ($left < 1) $left = 1; | |
7917 | if ($right < 1) $right = 1; | |
7918 | if ($right > this.options.maxPage - 1) $right = this.options.maxPage - 1; | |
7919 | ||
7920 | if ($linksBefore >= $half) { | |
7921 | $left -= $half; | |
7922 | } | |
7923 | else { | |
7924 | $left -= $linksBefore; | |
7925 | $right += $half - $linksBefore; | |
7926 | } | |
7927 | ||
7928 | if ($linksAfter >= $half) { | |
7929 | $right += $half; | |
7930 | } | |
7931 | else { | |
7932 | $right += $linksAfter; | |
7933 | $left -= $half - $linksAfter; | |
7934 | } | |
7935 | ||
7936 | $right = Math.ceil($right); | |
7937 | $left = Math.ceil($left); | |
7938 | if ($left < 1) $left = 1; | |
7939 | if ($right > this.options.maxPage) $right = this.options.maxPage; | |
7940 | ||
7941 | // left ... links | |
7942 | if ($left > 1) { | |
7943 | if ($left - 1 < 2) { | |
7944 | $pageList.append(this._renderLink(2)); | |
7945 | } | |
7946 | else { | |
3bdc6920 AE |
7947 | $('<li class="button jumpTo"><a title="' + WCF.Language.get('wcf.global.page.jumpTo') + '" class="jsTooltip">…</a></li>').appendTo($pageList); |
7948 | $hasHiddenPages = true; | |
158bd3ca TD |
7949 | } |
7950 | } | |
7951 | ||
7952 | // visible links | |
7953 | for (var $i = $left + 1; $i < $right; $i++) { | |
7954 | $pageList.append(this._renderLink($i)); | |
7955 | } | |
7956 | ||
7957 | // right ... links | |
7958 | if ($right < this.options.maxPage) { | |
7959 | if (this.options.maxPage - $right < 2) { | |
7960 | $pageList.append(this._renderLink(this.options.maxPage - 1)); | |
7961 | } | |
7962 | else { | |
3bdc6920 AE |
7963 | $('<li class="button jumpTo"><a title="' + WCF.Language.get('wcf.global.page.jumpTo') + '" class="jsTooltip">…</a></li>').appendTo($pageList); |
7964 | $hasHiddenPages = true; | |
158bd3ca TD |
7965 | } |
7966 | } | |
7967 | ||
7968 | // add last page | |
7969 | $pageList.append(this._renderLink(this.options.maxPage)); | |
7970 | ||
7971 | // add next button | |
b9698c4c | 7972 | var $nextElement = $('<li></li>').addClass('button skip'); |
158bd3ca TD |
7973 | $pageList.append($nextElement); |
7974 | ||
7975 | if (this.options.activePage < this.options.maxPage) { | |
b9698c4c | 7976 | var $nextLink = $('<a' + ((this.options.nextPage != null) ? (' title="' + this.options.nextPage + '"') : ('')) + '></a>'); |
158bd3ca TD |
7977 | $nextElement.append($nextLink); |
7978 | this._bindSwitchPage($nextLink, this.options.activePage + 1); | |
7979 | ||
7980 | var $nextImage = $('<img src="' + this.options.nextIcon + '" alt="" />'); | |
7981 | $nextLink.append($nextImage); | |
7982 | } | |
7983 | else { | |
5814a7f5 | 7984 | var $nextImage = $('<img src="' + this.options.nextIcon + '" alt="" />'); |
158bd3ca | 7985 | $nextElement.append($nextImage); |
be21c4eb | 7986 | $nextElement.addClass('disabled'); |
5814a7f5 | 7987 | $nextImage.addClass('disabled'); |
158bd3ca | 7988 | } |
5814a7f5 | 7989 | $nextImage.addClass('icon16'); |
3bdc6920 AE |
7990 | |
7991 | if ($hasHiddenPages) { | |
a19475d3 | 7992 | $pageList.data('pages', this.options.maxPage); |
3bdc6920 AE |
7993 | WCF.System.PageNavigation.init('#' + $pageList.wcfIdentify(), $.proxy(function(pageNo) { |
7994 | this.switchPage(pageNo); | |
7995 | }, this)); | |
3bdc6920 | 7996 | } |
158bd3ca TD |
7997 | } |
7998 | else { | |
7999 | // otherwise hide the paginator if not already hidden | |
8000 | this.element.hide(); | |
8001 | } | |
8002 | }, | |
8003 | ||
8004 | /** | |
9f959ced | 8005 | * Renders a page link. |
158bd3ca TD |
8006 | * |
8007 | * @parameter integer page | |
9f959ced | 8008 | * @return jQuery |
158bd3ca TD |
8009 | */ |
8010 | _renderLink: function(page, lineBreak) { | |
b9698c4c | 8011 | var $pageElement = $('<li class="button"></li>'); |
158bd3ca TD |
8012 | if (lineBreak != undefined && lineBreak) { |
8013 | $pageElement.addClass('break'); | |
8014 | } | |
8015 | if (page != this.options.activePage) { | |
8016 | var $pageLink = $('<a>' + WCF.String.addThousandsSeparator(page) + '</a>'); | |
8017 | $pageElement.append($pageLink); | |
8018 | this._bindSwitchPage($pageLink, page); | |
8019 | } | |
8020 | else { | |
8021 | $pageElement.addClass('active'); | |
8022 | var $pageSubElement = $('<span>' + WCF.String.addThousandsSeparator(page) + '</span>'); | |
8023 | $pageElement.append($pageSubElement); | |
8024 | } | |
8025 | ||
8026 | return $pageElement; | |
8027 | }, | |
8028 | ||
8029 | /** | |
8030 | * Binds the 'click'-event for the page switching to the given element. | |
8031 | * | |
8032 | * @parameter $(element) element | |
8033 | * @paremeter integer page | |
8034 | */ | |
8035 | _bindSwitchPage: function(element, page) { | |
8036 | var $self = this; | |
8037 | element.click(function() { | |
8038 | $self.switchPage(page); | |
8039 | }); | |
8040 | }, | |
8041 | ||
8042 | /** | |
8043 | * Switches to the given page | |
8044 | * | |
8045 | * @parameter Event event | |
8046 | * @parameter integer page | |
8047 | */ | |
8048 | switchPage: function(page) { | |
8049 | this._setOption('activePage', page); | |
8050 | }, | |
8051 | ||
8052 | /** | |
8053 | * Sets the given option to the given value. | |
8054 | * See the jQuery UI widget documentation for more. | |
8055 | */ | |
8056 | _setOption: function(key, value) { | |
8057 | if (key == 'activePage') { | |
8058 | if (value != this.options[key] && value > 0 && value <= this.options.maxPage) { | |
8059 | // you can prevent the page switching by returning false or by event.preventDefault() | |
8060 | // in a shouldSwitch-callback. e.g. if an AJAX request is already running. | |
8061 | var $result = this._trigger('shouldSwitch', undefined, { | |
81d1b3b5 | 8062 | nextPage: value |
158bd3ca TD |
8063 | }); |
8064 | ||
a19475d3 | 8065 | if ($result || $result !== undefined) { |
158bd3ca TD |
8066 | this.options[key] = value; |
8067 | this._render(); | |
8068 | this._trigger('switched', undefined, { | |
81d1b3b5 | 8069 | activePage: value |
158bd3ca TD |
8070 | }); |
8071 | } | |
8072 | else { | |
8073 | this._trigger('notSwitched', undefined, { | |
81d1b3b5 | 8074 | activePage: value |
158bd3ca TD |
8075 | }); |
8076 | } | |
8077 | } | |
8078 | } | |
8079 | else { | |
8080 | this.options[key] = value; | |
8081 | ||
8082 | if (key == 'disabled') { | |
8083 | if (value) { | |
8084 | this.element.children().remove(); | |
8085 | } | |
8086 | else { | |
f4126129 | 8087 | this._render(); |
158bd3ca TD |
8088 | } |
8089 | } | |
8090 | else if (key == 'maxPage') { | |
8091 | this._render(); | |
8092 | } | |
8093 | } | |
8094 | ||
8095 | return this; | |
8096 | }, | |
8097 | ||
8098 | /** | |
8099 | * Start input of pagenumber | |
8100 | * | |
8101 | * @parameter Event event | |
8102 | */ | |
8103 | _startInput: function(event) { | |
8104 | // hide a-tag | |
8105 | var $childLink = $(event.currentTarget); | |
8106 | if (!$childLink.is('a')) $childLink = $childLink.parent('a'); | |
8107 | ||
8108 | $childLink.hide(); | |
8109 | ||
8110 | // show input-tag | |
8111 | var $childInput = $childLink.parent('li').children('input') | |
8112 | .css('display', 'block') | |
8113 | .val(''); | |
8114 | ||
8115 | $childInput.focus(); | |
8116 | }, | |
8117 | ||
8118 | /** | |
8119 | * Stops input of pagenumber | |
8120 | * | |
8121 | * @parameter Event event | |
8122 | */ | |
8123 | _stopInput: function(event) { | |
8124 | // hide input-tag | |
8125 | var $childInput = $(event.currentTarget); | |
8126 | $childInput.css('display', 'none'); | |
8127 | ||
8128 | // show a-tag | |
f4126129 | 8129 | var $childContainer = $childInput.parent('li'); |
158bd3ca TD |
8130 | if ($childContainer != undefined && $childContainer != null) { |
8131 | $childContainer.children('a').show(); | |
8132 | } | |
8133 | }, | |
8134 | ||
8135 | /** | |
8136 | * Handles input of pagenumber | |
8137 | * | |
8138 | * @parameter Event event | |
8139 | */ | |
8140 | _handleInput: function(event) { | |
8141 | var $ie7 = ($.browser.msie && $.browser.version == '7.0'); | |
8142 | if (event.type != 'keyup' || $ie7) { | |
8143 | if (!$ie7 || ((event.which == 13 || event.which == 27) && event.type == 'keyup')) { | |
8144 | if (event.which == 13) { | |
8145 | this.switchPage(parseInt($(event.currentTarget).val())); | |
8146 | } | |
8147 | ||
8148 | if (event.which == 13 || event.which == 27) { | |
8149 | this._stopInput(event); | |
8150 | event.stopPropagation(); | |
8151 | } | |
8152 | } | |
8153 | } | |
8154 | } | |
8155 | }); | |
8156 | ||
8157 | /** | |
8158 | * Encapsulate eval() within an own function to prevent problems | |
8159 | * with optimizing and minifiny JS. | |
8160 | * | |
8161 | * @param mixed expression | |
8162 | * @returns mixed | |
8163 | */ | |
8164 | function wcfEval(expression) { | |
8165 | return eval(expression); | |
8166 | } |