Fixed time zone calculation issue
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WCF.Message.js
1 /**
2 * Message related classes for WCF
3 *
4 * @author Alexander Ebert
5 * @copyright 2001-2014 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 */
8 WCF.Message = { };
9
10 /**
11 * Namespace for BBCode related classes.
12 */
13 WCF.Message.BBCode = { };
14
15 /**
16 * BBCode Viewer for WCF.
17 */
18 WCF.Message.BBCode.CodeViewer = Class.extend({
19 /**
20 * dialog overlay
21 * @var jQuery
22 */
23 _dialog: null,
24
25 /**
26 * Initializes the WCF.Message.BBCode.CodeViewer class.
27 */
28 init: function() {
29 this._dialog = null;
30
31 this._initCodeBoxes();
32
33 WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.BBCode.CodeViewer', $.proxy(this._initCodeBoxes, this));
34 WCF.DOMNodeInsertedHandler.execute();
35 },
36
37 /**
38 * Initializes available code boxes.
39 */
40 _initCodeBoxes: function() {
41 $('.codeBox:not(.jsCodeViewer)').each($.proxy(function(index, codeBox) {
42 var $codeBox = $(codeBox).addClass('jsCodeViewer');
43
44 $('<span class="icon icon16 icon-copy pointer jsTooltip" title="' + WCF.Language.get('wcf.message.bbcode.code.copy') + '" />').appendTo($codeBox.find('div > h3')).click($.proxy(this._click, this));
45 }, this));
46 },
47
48 /**
49 * Shows a code viewer for a specific code box.
50 *
51 * @param object event
52 */
53 _click: function(event) {
54 var $content = '';
55 $(event.currentTarget).parents('div').next('ol').children('li').each(function(index, listItem) {
56 if ($content) {
57 $content += "\n";
58 }
59
60 // do *not* use $.trim here, as we want to preserve whitespaces
61 $content += $(listItem).text().replace(/\n+$/, '');
62 });
63
64 if (this._dialog === null) {
65 this._dialog = $('<div><textarea cols="60" rows="12" readonly="readonly" /></div>').hide().appendTo(document.body);
66 this._dialog.children('textarea').val($content);
67 this._dialog.wcfDialog({
68 title: WCF.Language.get('wcf.message.bbcode.code.copy')
69 });
70 }
71 else {
72 this._dialog.children('textarea').val($content);
73 this._dialog.wcfDialog('open');
74 }
75
76 this._dialog.children('textarea').select();
77 }
78 });
79
80 /**
81 * Prevents multiple submits of the same form by disabling the submit button.
82 */
83 WCF.Message.FormGuard = Class.extend({
84 /**
85 * Initializes the WCF.Message.FormGuard class.
86 */
87 init: function() {
88 var $forms = $('form.jsFormGuard').removeClass('jsFormGuard').submit(function() {
89 $(this).find('.formSubmit input[type=submit]').disable();
90 });
91
92 // restore buttons, prevents disabled buttons on back navigation in Opera
93 $(window).unload(function() {
94 $forms.find('.formSubmit input[type=submit]').enable();
95 });
96 }
97 });
98
99 /**
100 * Provides previews for ckEditor message fields.
101 *
102 * @param string className
103 * @param string messageFieldID
104 * @param string previewButtonID
105 */
106 WCF.Message.Preview = Class.extend({
107 /**
108 * class name
109 * @var string
110 */
111 _className: '',
112
113 /**
114 * message field id
115 * @var string
116 */
117 _messageFieldID: '',
118
119 /**
120 * message field
121 * @var jQuery
122 */
123 _messageField: null,
124
125 /**
126 * action proxy
127 * @var WCF.Action.Proxy
128 */
129 _proxy: null,
130
131 /**
132 * preview button
133 * @var jQuery
134 */
135 _previewButton: null,
136
137 /**
138 * previous button label
139 * @var string
140 */
141 _previewButtonLabel: '',
142
143 /**
144 * Initializes a new WCF.Message.Preview object.
145 *
146 * @param string className
147 * @param string messageFieldID
148 * @param string previewButtonID
149 */
150 init: function(className, messageFieldID, previewButtonID) {
151 this._className = className;
152
153 // validate message field
154 this._messageFieldID = $.wcfEscapeID(messageFieldID);
155 this._messageField = $('#' + this._messageFieldID);
156 if (!this._messageField.length) {
157 console.debug("[WCF.Message.Preview] Unable to find message field identified by '" + this._messageFieldID + "'");
158 return;
159 }
160
161 // validate preview button
162 previewButtonID = $.wcfEscapeID(previewButtonID);
163 this._previewButton = $('#' + previewButtonID);
164 if (!this._previewButton.length) {
165 console.debug("[WCF.Message.Preview] Unable to find preview button identified by '" + previewButtonID + "'");
166 return;
167 }
168
169 this._previewButton.click($.proxy(this._click, this));
170 this._proxy = new WCF.Action.Proxy({
171 failure: $.proxy(this._failure, this),
172 success: $.proxy(this._success, this)
173 });
174 },
175
176 /**
177 * Reads message field input and triggers an AJAX request.
178 */
179 _click: function(event) {
180 var $message = this._getMessage();
181 if ($message === null) {
182 console.debug("[WCF.Message.Preview] Unable to access ckEditor instance of '" + this._messageFieldID + "'");
183 return;
184 }
185
186 this._proxy.setOption('data', {
187 actionName: 'getMessagePreview',
188 className: this._className,
189 parameters: this._getParameters($message)
190 });
191 this._proxy.sendRequest();
192
193 // update button label
194 this._previewButtonLabel = this._previewButton.html();
195 this._previewButton.html(WCF.Language.get('wcf.global.loading')).disable();
196
197 // poke event
198 event.stopPropagation();
199 return false;
200 },
201
202 /**
203 * Returns request parameters.
204 *
205 * @param string message
206 * @return object
207 */
208 _getParameters: function(message) {
209 // collect message form options
210 var $options = { };
211 $('#settings').find('input[type=checkbox]').each(function(index, checkbox) {
212 var $checkbox = $(checkbox);
213 if ($checkbox.is(':checked')) {
214 $options[$checkbox.prop('name')] = $checkbox.prop('value');
215 }
216 });
217
218 // build parameters
219 return {
220 data: {
221 message: message
222 },
223 options: $options
224 };
225 },
226
227 /**
228 * Returns parsed message from ckEditor or null if editor was not accessible.
229 *
230 * @return string
231 */
232 _getMessage: function() {
233 if (!$.browser.ckeditor) {
234 return this._messageField.val();
235 }
236 else if (this._messageField.data('ckeditorInstance')) {
237 var $ckEditor = this._messageField.ckeditorGet();
238 return $ckEditor.getData();
239 }
240
241 return null;
242 },
243
244 /**
245 * Handles successful AJAX requests.
246 *
247 * @param object data
248 * @param string textStatus
249 * @param jQuery jqXHR
250 */
251 _success: function(data, textStatus, jqXHR) {
252 // restore preview button
253 this._previewButton.html(this._previewButtonLabel).enable();
254
255 // remove error message
256 this._messageField.parent().children('small.innerError').remove();
257
258 // evaluate message
259 this._handleResponse(data);
260 },
261
262 /**
263 * Evaluates response data.
264 *
265 * @param object data
266 */
267 _handleResponse: function(data) { },
268
269 /**
270 * Handles errors during preview requests.
271 *
272 * The return values indicates if the default error overlay is shown.
273 *
274 * @param object data
275 * @return boolean
276 */
277 _failure: function(data) {
278 if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
279 return true;
280 }
281
282 // restore preview button
283 this._previewButton.html(this._previewButtonLabel).enable();
284
285 var $innerError = this._messageField.next('small.innerError').empty();
286 if (!$innerError.length) {
287 $innerError = $('<small class="innerError" />').appendTo(this._messageField.parent());
288 }
289
290 $innerError.html(data.returnValues.errorType);
291
292 return false;
293 }
294 });
295
296 /**
297 * Default implementation for message previews.
298 *
299 * @see WCF.Message.Preview
300 */
301 WCF.Message.DefaultPreview = WCF.Message.Preview.extend({
302 _attachmentObjectType: null,
303 _attachmentObjectID: null,
304 _tmpHash: null,
305
306 /**
307 * @see WCF.Message.Preview.init()
308 */
309 init: function(attachmentObjectType, attachmentObjectID, tmpHash) {
310 this._super('wcf\\data\\bbcode\\MessagePreviewAction', 'text', 'previewButton');
311
312 this._attachmentObjectType = attachmentObjectType || null;
313 this._attachmentObjectID = attachmentObjectID || null;
314 this._tmpHash = tmpHash || null;
315 },
316
317 /**
318 * @see WCF.Message.Preview._handleResponse()
319 */
320 _handleResponse: function(data) {
321 var $preview = $('#previewContainer');
322 if (!$preview.length) {
323 $preview = $('<div class="container containerPadding marginTop" id="previewContainer"><fieldset><legend>' + WCF.Language.get('wcf.global.preview') + '</legend><div></div></fieldset>').prependTo($('#messageContainer')).wcfFadeIn();
324 }
325
326 $preview.find('div:eq(0)').html(data.returnValues.message);
327
328 new WCF.Effect.Scroll().scrollTo($preview);
329 },
330
331 /**
332 * @see WCF.Message.Preview._getParameters()
333 */
334 _getParameters: function(message) {
335 var $parameters = this._super(message);
336
337 if (this._attachmentObjectType != null) {
338 $parameters.attachmentObjectType = this._attachmentObjectType;
339 $parameters.attachmentObjectID = this._attachmentObjectID;
340 $parameters.tmpHash = this._tmpHash;
341 }
342
343 return $parameters;
344 }
345 });
346
347 /**
348 * Handles multilingualism for messages.
349 *
350 * @param integer languageID
351 * @param object availableLanguages
352 * @param boolean forceSelection
353 */
354 WCF.Message.Multilingualism = Class.extend({
355 /**
356 * list of available languages
357 * @var object
358 */
359 _availableLanguages: { },
360
361 /**
362 * language id
363 * @var integer
364 */
365 _languageID: 0,
366
367 /**
368 * language input element
369 * @var jQuery
370 */
371 _languageInput: null,
372
373 /**
374 * Initializes WCF.Message.Multilingualism
375 *
376 * @param integer languageID
377 * @param object availableLanguages
378 * @param boolean forceSelection
379 */
380 init: function(languageID, availableLanguages, forceSelection) {
381 this._availableLanguages = availableLanguages;
382 this._languageID = languageID || 0;
383
384 this._languageInput = $('#languageID');
385
386 // preselect current language id
387 this._updateLabel();
388
389 // register event listener
390 this._languageInput.find('.dropdownMenu > li').click($.proxy(this._click, this));
391
392 // add element to disable multilingualism
393 if (!forceSelection) {
394 var $dropdownMenu = this._languageInput.find('.dropdownMenu');
395 $('<li class="dropdownDivider" />').appendTo($dropdownMenu);
396 $('<li><span><span class="badge">' + this._availableLanguages[0] + '</span></span></li>').click($.proxy(this._disable, this)).appendTo($dropdownMenu);
397 }
398
399 // bind submit event
400 this._languageInput.parents('form').submit($.proxy(this._submit, this));
401 },
402
403 /**
404 * Handles language selections.
405 *
406 * @param object event
407 */
408 _click: function(event) {
409 this._languageID = $(event.currentTarget).data('languageID');
410 this._updateLabel();
411 },
412
413 /**
414 * Disables language selection.
415 */
416 _disable: function() {
417 this._languageID = 0;
418 this._updateLabel();
419 },
420
421 /**
422 * Updates selected language.
423 */
424 _updateLabel: function() {
425 this._languageInput.find('.dropdownToggle > span').text(this._availableLanguages[this._languageID]);
426 },
427
428 /**
429 * Sets language id upon submit.
430 */
431 _submit: function() {
432 this._languageInput.next('input[name=languageID]').prop('value', this._languageID);
433 }
434 });
435
436 /**
437 * Loads smiley categories upon user request.
438 */
439 WCF.Message.SmileyCategories = Class.extend({
440 /**
441 * list of already loaded category ids
442 * @var array<integer>
443 */
444 _cache: [ ],
445
446 /**
447 * action proxy
448 * @var WCF.Action.Proxy
449 */
450 _proxy: null,
451
452 /**
453 * ckEditor element
454 * @var jQuery
455 */
456 _ckEditor: null,
457
458 /**
459 * Initializes the smiley loader.
460 *
461 * @param string ckEditorID
462 */
463 init: function() {
464 this._cache = [ ];
465 this._proxy = new WCF.Action.Proxy({
466 success: $.proxy(this._success, this)
467 });
468
469 $('#smilies').on('wcftabsbeforeactivate', $.proxy(this._click, this));
470
471 // handle onload
472 var self = this;
473 new WCF.PeriodicalExecuter(function(pe) {
474 pe.stop();
475
476 self._click({ }, { newTab: $('#smilies > .menu li.ui-state-active') });
477 }, 100);
478 },
479
480 /**
481 * Handles tab menu clicks.
482 *
483 * @param object event
484 * @param object ui
485 */
486 _click: function(event, ui) {
487 var $categoryID = parseInt($(ui.newTab).children('a').data('smileyCategoryID'));
488
489 if ($categoryID && !WCF.inArray($categoryID, this._cache)) {
490 this._proxy.setOption('data', {
491 actionName: 'getSmilies',
492 className: 'wcf\\data\\smiley\\category\\SmileyCategoryAction',
493 objectIDs: [ $categoryID ]
494 });
495 this._proxy.sendRequest();
496 }
497 },
498
499 /**
500 * Handles successful AJAX requests.
501 *
502 * @param object data
503 * @param string textStatus
504 * @param jQuery jqXHR
505 */
506 _success: function(data, textStatus, jqXHR) {
507 var $categoryID = parseInt(data.returnValues.smileyCategoryID);
508 this._cache.push($categoryID);
509
510 $('#smilies-' + $categoryID).html(data.returnValues.template);
511 }
512 });
513
514 /**
515 * Handles smiley clicks.
516 */
517 WCF.Message.Smilies = Class.extend({
518 /**
519 * ckEditor element
520 * @var jQuery
521 */
522 _ckEditor: null,
523
524 /**
525 * Initializes the smiley handler.
526 *
527 * @param string ckEditorID
528 */
529 init: function(ckEditorID) {
530 // get ck editor
531 if (ckEditorID) {
532 this._ckEditor = $('#' + ckEditorID);
533
534 // add smiley click handler
535 $(document).on('click', '.jsSmiley', $.proxy(this._smileyClick, this));
536 }
537 },
538
539 /**
540 * Handles tab smiley clicks.
541 *
542 * @param object event
543 */
544 _smileyClick: function(event) {
545 var $target = $(event.currentTarget);
546 var $smileyCode = $target.data('smileyCode');
547 var $smileyPath = $target.data('smileyPath');
548
549 // get ckEditor
550 var $ckEditor = this._ckEditor.ckeditorGet();
551
552 // add smiley to config
553 if (!WCF.inArray($smileyCode, $ckEditor.config.smiley_descriptions)) {
554 $ckEditor.config.smiley_descriptions.push($smileyCode);
555 $ckEditor.config.smiley_images.push($smileyPath);
556 }
557
558 if ($ckEditor.mode === 'wysiwyg') {
559 // in design mode
560 var $img = $ckEditor.document.createElement('img', {
561 attributes: {
562 src: $ckEditor.config.smiley_path + $smileyPath,
563 'class': 'smiley',
564 alt: $smileyCode
565 }
566 });
567 $ckEditor.insertText(' ');
568 $ckEditor.insertElement($img);
569 $ckEditor.insertText(' ');
570 }
571 else {
572 // in source mode
573 var $textarea = this._ckEditor.next('.cke_editor_text').find('textarea');
574 var $value = $textarea.val();
575 if ($value.length == 0) {
576 $textarea.val($smileyCode);
577 $textarea.setCaret($smileyCode.length);
578 }
579 else {
580 var $position = $textarea.getCaret();
581 var $string = (($value.substr($position - 1, 1) !== ' ') ? ' ' : '') + $smileyCode + ' ';
582 $textarea.val( $value.substr(0, $position) + $string + $value.substr($position) );
583 $textarea.setCaret($position + $string.length);
584 }
585 }
586 }
587 });
588
589 /**
590 * Provides an AJAX-based quick reply for messages.
591 */
592 WCF.Message.QuickReply = Class.extend({
593 /**
594 * quick reply container
595 * @var jQuery
596 */
597 _container: null,
598
599 /**
600 * message field
601 * @var jQuery
602 */
603 _messageField: null,
604
605 /**
606 * notification object
607 * @var WCF.System.Notification
608 */
609 _notification: null,
610
611 /**
612 * true, if a request to save the message is pending
613 * @var boolean
614 */
615 _pendingSave: false,
616
617 /**
618 * action proxy
619 * @var WCF.Action.Proxy
620 */
621 _proxy: null,
622
623 /**
624 * quote manager object
625 * @var WCF.Message.Quote.Manager
626 */
627 _quoteManager: null,
628
629 /**
630 * scroll handler
631 * @var WCF.Effect.Scroll
632 */
633 _scrollHandler: null,
634
635 /**
636 * success message for created but invisible messages
637 * @var string
638 */
639 _successMessageNonVisible: '',
640
641 /**
642 * Initializes a new WCF.Message.QuickReply object.
643 *
644 * @param boolean supportExtendedForm
645 * @param WCF.Message.Quote.Manager quoteManager
646 */
647 init: function(supportExtendedForm, quoteManager) {
648 this._container = $('#messageQuickReply');
649 this._container.children('.message').addClass('jsInvalidQuoteTarget');
650 this._messageField = $('#text');
651 this._pendingSave = false;
652 if (!this._container || !this._messageField) {
653 return;
654 }
655
656 // button actions
657 var $formSubmit = this._container.find('.formSubmit');
658 $formSubmit.find('button[data-type=save]').click($.proxy(this._save, this));
659 if (supportExtendedForm) $formSubmit.find('button[data-type=extended]').click($.proxy(this._prepareExtended, this));
660 $formSubmit.find('button[data-type=cancel]').click($.proxy(this._cancel, this));
661
662 if (quoteManager) this._quoteManager = quoteManager;
663
664 $('.jsQuickReply').data('__api', this).click($.proxy(this.click, this));
665
666 this._proxy = new WCF.Action.Proxy({
667 failure: $.proxy(this._failure, this),
668 showLoadingOverlay: false,
669 success: $.proxy(this._success, this)
670 });
671 this._scroll = new WCF.Effect.Scroll();
672 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.add'));
673 this._successMessageNonVisible = '';
674 },
675
676 /**
677 * Handles clicks on reply button.
678 *
679 * @param object event
680 */
681 click: function(event) {
682 this._container.toggle();
683
684 if (this._container.is(':visible')) {
685 this._scroll.scrollTo(this._container, true);
686
687 WCF.Message.Submit.registerButton('text', this._container.find('.formSubmit button[data-type=save]'));
688
689 if (this._quoteManager) {
690 // check if message field is empty
691 var $empty = true;
692 if ($.browser.ckeditor) {
693 var self = this;
694 this._messageField.ckeditor(function() {
695 $empty = (!$.trim(this.getData()).length);
696 self._ckeditorCallback($empty);
697 });
698
699 }
700 else {
701 $empty = (!this._messageField.val().length);
702 this._ckeditorCallback($empty);
703 }
704 }
705 }
706
707 // discard event
708 if (event !== null) {
709 event.stopPropagation();
710 return false;
711 }
712 },
713
714 _ckeditorCallback: function(isEmpty) {
715 if (isEmpty) {
716 this._quoteManager.insertQuotes(this._getClassName(), this._getObjectID(), $.proxy(this._insertQuotes, this));
717 }
718
719 if ($.browser.ckeditor) {
720 this._messageField.ckeditorGet().ui.editor.focus();
721 }
722 else {
723 this._messageField.focus();
724 }
725 },
726
727 /**
728 * Returns container element.
729 *
730 * @return jQuery
731 */
732 getContainer: function() {
733 return this._container;
734 },
735
736 /**
737 * Insertes quotes into the quick reply editor.
738 *
739 * @param object data
740 */
741 _insertQuotes: function(data) {
742 if (!data.returnValues.template) {
743 return;
744 }
745
746 if ($.browser.ckeditor) {
747 var $ckEditor = this._messageField.ckeditorGet();
748
749 // work-around for a strange selection bug in Firefox: http://www.woltlab.com/forum/index.php/Thread/220522-Zitat-Fehler/
750 if ($ckEditor.getSelection().getStartElement() === null) {
751 // range is broken, set it to end of text: http://stackoverflow.com/a/16308194
752 var $range = $ckEditor.createRange();
753 $range.moveToPosition($range.root, CKEDITOR.POSITION_BEFORE_END);
754 $ckEditor.getSelection().selectRanges([ $range ]);
755 }
756
757 $ckEditor.insertText(data.returnValues.template);
758 }
759 else {
760 this._messageField.val(data.returnValues.template);
761 }
762 },
763
764 /**
765 * Saves message.
766 */
767 _save: function() {
768 if (this._pendingSave) {
769 return;
770 }
771
772 var $message = '';
773
774 if ($.browser.ckeditor) {
775 var $ckEditor = this._messageField.ckeditorGet();
776 $message = $.trim($ckEditor.getData());
777 }
778 else {
779 $message = $.trim(this._messageField.val());
780 }
781
782 // check if message is empty
783 var $innerError = this._messageField.parent().find('small.innerError');
784 if ($message === '' || $message === '0') {
785 if (!$innerError.length) {
786 $innerError = $('<small class="innerError" />').appendTo(this._messageField.parent());
787 }
788
789 $innerError.html(WCF.Language.get('wcf.global.form.error.empty'));
790 return;
791 }
792 else {
793 $innerError.remove();
794 }
795
796 this._pendingSave = true;
797
798 this._proxy.setOption('data', {
799 actionName: 'quickReply',
800 className: this._getClassName(),
801 interfaceName: 'wcf\\data\\IMessageQuickReplyAction',
802 parameters: this._getParameters($message)
803 });
804 this._proxy.sendRequest();
805
806 // show spinner and hide CKEditor
807 var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
808 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody);
809 $messageBody.children('#cke_text').hide().end().next().hide();
810 },
811
812 /**
813 * Returns the parameters for the save request.
814 *
815 * @param string message
816 * @return object
817 */
818 _getParameters: function(message) {
819 var $parameters = {
820 objectID: this._getObjectID(),
821 data: {
822 message: message
823 },
824 lastPostTime: this._container.data('lastPostTime'),
825 pageNo: this._container.data('pageNo'),
826 removeQuoteIDs: (this._quoteManager === null ? [ ] : this._quoteManager.getQuotesMarkedForRemoval())
827 };
828 if (this._container.data('anchor')) {
829 $parameters.anchor = this._container.data('anchor');
830 }
831
832 return $parameters;
833 },
834
835 /**
836 * Cancels quick reply.
837 */
838 _cancel: function() {
839 this._revertQuickReply(true);
840
841 if ($.browser.ckeditor) {
842 // revert CKEditor
843 this._messageField.ckeditorGet().setData('');
844 }
845 else {
846 this._messageField.val('');
847 }
848 },
849
850 /**
851 * Reverts quick reply to original state and optionally hiding it.
852 *
853 * @param boolean hide
854 */
855 _revertQuickReply: function(hide) {
856 var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
857
858 if (hide) {
859 this._container.hide();
860
861 // remove previous error messages
862 $messageBody.children('small.innerError').remove();
863 }
864
865 // display CKEditor
866 $messageBody.children('.icon-spinner').remove();
867 $messageBody.children('#cke_text').show();
868
869 // display form submit
870 $messageBody.next().show();
871 },
872
873 /**
874 * Prepares jump to extended message add form.
875 */
876 _prepareExtended: function() {
877 this._pendingSave = true;
878
879 // mark quotes for removal
880 if (this._quoteManager !== null) {
881 this._quoteManager.markQuotesForRemoval();
882 }
883
884 var $message = '';
885
886 if ($.browser.ckeditor) {
887 var $ckEditor = this._messageField.ckeditorGet();
888 $message = $ckEditor.getData();
889 }
890 else {
891 $message = this._messageField.val();
892 }
893
894 new WCF.Action.Proxy({
895 autoSend: true,
896 data: {
897 actionName: 'jumpToExtended',
898 className: this._getClassName(),
899 interfaceName: 'wcf\\data\\IExtendedMessageQuickReplyAction',
900 parameters: {
901 containerID: this._getObjectID(),
902 message: $message
903 }
904 },
905 success: function(data, textStatus, jqXHR) {
906 window.location = data.returnValues.url;
907 }
908 });
909 },
910
911 /**
912 * Handles successful AJAX calls.
913 *
914 * @param object data
915 * @param string textStatus
916 * @param jQuery jqXHR
917 */
918 _success: function(data, textStatus, jqXHR) {
919 // redirect to new page
920 if (data.returnValues.url) {
921 window.location = data.returnValues.url;
922 }
923 else {
924 if (data.returnValues.template) {
925 // insert HTML
926 var $message = $('' + data.returnValues.template);
927 if (this._container.data('sortOrder') == 'DESC') {
928 $message.insertAfter(this._container);
929 }
930 else {
931 $message.insertBefore(this._container);
932 }
933
934 // update last post time
935 this._container.data('lastPostTime', data.returnValues.lastPostTime);
936
937 // show notification
938 this._notification.show(undefined, undefined, WCF.Language.get('wcf.global.success.add'));
939
940 this._updateHistory($message.wcfIdentify());
941 }
942 else {
943 // show notification
944 var $message = (this._successMessageNonVisible) ? this._successMessageNonVisible : 'wcf.global.success.add';
945 this._notification.show(undefined, 5000, WCF.Language.get($message));
946 }
947
948 if ($.browser.ckeditor) {
949 // remove CKEditor contents
950 this._messageField.ckeditorGet().setData('');
951 }
952 else {
953 this._messageField.val('');
954 }
955
956 // hide quick reply and revert it
957 this._revertQuickReply(true);
958
959 // count stored quotes
960 if (this._quoteManager !== null) {
961 this._quoteManager.countQuotes();
962 }
963
964 this._pendingSave = false;
965 }
966 },
967
968 /**
969 * Reverts quick reply on failure to preserve entered message.
970 */
971 _failure: function(data) {
972 this._pendingSave = false;
973 this._revertQuickReply(false);
974
975 if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
976 return true;
977 }
978
979 var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
980 var $innerError = $messageBody.children('small.innerError').empty();
981 if (!$innerError.length) {
982 $innerError = $('<small class="innerError" />').appendTo($messageBody);
983 }
984
985 $innerError.html(data.returnValues.errorType);
986
987 return false;
988 },
989
990 /**
991 * Returns action class name.
992 *
993 * @return string
994 */
995 _getClassName: function() {
996 return '';
997 },
998
999 /**
1000 * Returns object id.
1001 *
1002 * @return integer
1003 */
1004 _getObjectID: function() {
1005 return 0;
1006 },
1007
1008 /**
1009 * Updates the history to avoid old content when going back in the browser
1010 * history.
1011 *
1012 * @param hash
1013 */
1014 _updateHistory: function(hash) {
1015 window.location.hash = hash;
1016 }
1017 });
1018
1019 /**
1020 * Provides an inline message editor.
1021 *
1022 * @param integer containerID
1023 */
1024 WCF.Message.InlineEditor = Class.extend({
1025 /**
1026 * currently active message
1027 * @var string
1028 */
1029 _activeElementID: '',
1030
1031 /**
1032 * message cache
1033 * @var string
1034 */
1035 _cache: '',
1036
1037 /**
1038 * list of messages
1039 * @var object
1040 */
1041 _container: { },
1042
1043 /**
1044 * container id
1045 * @var integer
1046 */
1047 _containerID: 0,
1048
1049 /**
1050 * list of dropdowns
1051 * @var object
1052 */
1053 _dropdowns: { },
1054
1055 /**
1056 * CSS selector for the message container
1057 * @var string
1058 */
1059 _messageContainerSelector: '.jsMessage',
1060
1061 /**
1062 * prefix of the message editor CSS id
1063 * @var string
1064 */
1065 _messageEditorIDPrefix: 'messageEditor',
1066
1067 /**
1068 * notification object
1069 * @var WCF.System.Notification
1070 */
1071 _notification: null,
1072
1073 /**
1074 * proxy object
1075 * @var WCF.Action.Proxy
1076 */
1077 _proxy: null,
1078
1079 /**
1080 * quote manager object
1081 * @var WCF.Message.Quote.Manager
1082 */
1083 _quoteManager: null,
1084
1085 /**
1086 * support for extended editing form
1087 * @var boolean
1088 */
1089 _supportExtendedForm: false,
1090
1091 /**
1092 * Initializes a new WCF.Message.InlineEditor object.
1093 *
1094 * @param integer containerID
1095 * @param boolean supportExtendedForm
1096 * @param WCF.Message.Quote.Manager quoteManager
1097 */
1098 init: function(containerID, supportExtendedForm, quoteManager) {
1099 this._activeElementID = '';
1100 this._cache = '';
1101 this._container = { };
1102 this._containerID = parseInt(containerID);
1103 this._dropdowns = { };
1104 this._quoteManager = quoteManager || null;
1105 this._supportExtendedForm = (supportExtendedForm) ? true : false;
1106 this._proxy = new WCF.Action.Proxy({
1107 failure: $.proxy(this._failure, this),
1108 showLoadingOverlay: false,
1109 success: $.proxy(this._success, this)
1110 });
1111 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.edit'));
1112
1113 this.initContainers();
1114
1115 WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.InlineEditor', $.proxy(this.initContainers, this));
1116 },
1117
1118 /**
1119 * Initializes editing capability for all messages.
1120 */
1121 initContainers: function() {
1122 $(this._messageContainerSelector).each($.proxy(function(index, container) {
1123 var $container = $(container);
1124 var $containerID = $container.wcfIdentify();
1125
1126 if (!this._container[$containerID]) {
1127 this._container[$containerID] = $container;
1128
1129 if ($container.data('canEditInline')) {
1130 var $button = $container.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID).click($.proxy(this._clickInline, this));
1131 if ($container.data('canEdit')) $button.dblclick($.proxy(this._click, this));
1132 }
1133 else if ($container.data('canEdit')) {
1134 $container.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID).click($.proxy(this._click, this));
1135 }
1136 }
1137 }, this));
1138 },
1139
1140 /**
1141 * Loads WYSIWYG editor for selected message.
1142 *
1143 * @param object event
1144 * @param integer containerID
1145 * @return boolean
1146 */
1147 _click: function(event, containerID) {
1148 var $containerID = (event === null) ? containerID : $(event.currentTarget).data('containerID');
1149 if (this._activeElementID === '') {
1150 this._activeElementID = $containerID;
1151 this._prepare();
1152
1153 this._proxy.setOption('data', {
1154 actionName: 'beginEdit',
1155 className: this._getClassName(),
1156 interfaceName: 'wcf\\data\\IMessageInlineEditorAction',
1157 parameters: {
1158 containerID: this._containerID,
1159 objectID: this._container[$containerID].data('objectID')
1160 }
1161 });
1162 this._proxy.setOption('failure', $.proxy(function() { this._cancel(); }, this));
1163 this._proxy.sendRequest();
1164 }
1165 else {
1166 var $notification = new WCF.System.Notification(WCF.Language.get('wcf.message.error.editorAlreadyInUse'), 'warning');
1167 $notification.show();
1168 }
1169
1170 // force closing dropdown to avoid displaying the dropdown after
1171 // triple clicks
1172 if (this._dropdowns[this._container[$containerID].data('objectID')]) {
1173 this._dropdowns[this._container[$containerID].data('objectID')].removeClass('dropdownOpen');
1174 }
1175
1176 if (event !== null) {
1177 event.stopPropagation();
1178 return false;
1179 }
1180 },
1181
1182 /**
1183 * Provides an inline dropdown menu instead of directly loading the WYSIWYG editor.
1184 *
1185 * @param object event
1186 * @return boolean
1187 */
1188 _clickInline: function(event) {
1189 var $button = $(event.currentTarget);
1190
1191 if (!$button.hasClass('dropdownToggle')) {
1192 var $containerID = $button.data('containerID');
1193
1194 $button.addClass('dropdownToggle').parent().addClass('dropdown');
1195
1196 var $dropdownMenu = $('<ul class="dropdownMenu" />').insertAfter($button);
1197 this._initDropdownMenu($containerID, $dropdownMenu);
1198
1199 WCF.DOMNodeInsertedHandler.execute();
1200
1201 this._dropdowns[this._container[$containerID].data('objectID')] = $dropdownMenu;
1202
1203 WCF.Dropdown.registerCallback($button.parent().wcfIdentify(), $.proxy(this._toggleDropdown, this));
1204
1205 // trigger click event
1206 $button.trigger('click');
1207 }
1208
1209 event.stopPropagation();
1210 return false;
1211 },
1212
1213 /**
1214 * Handles errorneus editing requests.
1215 *
1216 * @param object data
1217 */
1218 _failure: function(data) {
1219 this._revertEditor();
1220
1221 if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
1222 return true;
1223 }
1224
1225 var $messageBody = this._container[this._activeElementID].find('.messageBody .messageInlineEditor');
1226 var $innerError = $messageBody.children('small.innerError').empty();
1227 if (!$innerError.length) {
1228 $innerError = $('<small class="innerError" />').insertBefore($messageBody.children('.formSubmit'));
1229 }
1230
1231 $innerError.html(data.returnValues.errorType);
1232
1233 return false;
1234 },
1235
1236 /**
1237 * Forces message options to stay visible if toggling dropdown menu.
1238 *
1239 * @param string containerID
1240 * @param string action
1241 */
1242 _toggleDropdown: function(containerID, action) {
1243 WCF.Dropdown.getDropdown(containerID).parents('.messageOptions').toggleClass('forceOpen');
1244 },
1245
1246 /**
1247 * Initializes the inline edit dropdown menu.
1248 *
1249 * @param integer containerID
1250 * @param jQuery dropdownMenu
1251 */
1252 _initDropdownMenu: function(containerID, dropdownMenu) { },
1253
1254 /**
1255 * Prepares message for WYSIWYG display.
1256 */
1257 _prepare: function() {
1258 var $messageBody = this._container[this._activeElementID].find('.messageBody');
1259 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody);
1260
1261 var $content = $messageBody.find('.messageText');
1262
1263 // hide unrelated content
1264 $content.parent().children('.jsInlineEditorHideContent').hide();
1265
1266 this._cache = $content.detach();
1267 },
1268
1269 /**
1270 * Cancels editing and reverts to original message.
1271 */
1272 _cancel: function() {
1273 var $container = this._container[this._activeElementID].removeClass('jsInvalidQuoteTarget');
1274
1275 // remove ckEditor
1276 try {
1277 var $ckEditor = $('#' + this._messageEditorIDPrefix + $container.data('objectID')).ckeditorGet();
1278 $ckEditor.destroy();
1279 }
1280 catch (e) {
1281 // CKEditor might be not initialized yet, ignore
1282 }
1283
1284 // restore message
1285 var $messageBody = $container.find('.messageBody');
1286 $messageBody.children('.icon-spinner').remove();
1287 $messageBody.children('div:eq(0)').html(this._cache);
1288
1289 // show unrelated content
1290 $messageBody.find('.jsInlineEditorHideContent').show();
1291
1292 // revert message options
1293 this._container[this._activeElementID].find('.messageOptions').removeClass('forceHidden');
1294
1295 this._activeElementID = '';
1296
1297 if (this._quoteManager) {
1298 this._quoteManager.clearAlternativeCKEditor();
1299 }
1300 },
1301
1302 /**
1303 * Handles successful AJAX calls.
1304 *
1305 * @param object data
1306 * @param string textStatus
1307 * @param jQuery jqXHR
1308 */
1309 _success: function(data, textStatus, jqXHR) {
1310 switch (data.returnValues.actionName) {
1311 case 'beginEdit':
1312 this._showEditor(data);
1313 break;
1314
1315 case 'save':
1316 this._showMessage(data);
1317 break;
1318 }
1319 },
1320
1321 /**
1322 * Shows WYSIWYG editor for active message.
1323 *
1324 * @param object data
1325 */
1326 _showEditor: function(data) {
1327 // revert failure function
1328 this._proxy.setOption('failure', $.proxy(this._failure, this));
1329
1330 var $messageBody = this._container[this._activeElementID].addClass('jsInvalidQuoteTarget').find('.messageBody');
1331 $messageBody.children('.icon-spinner').remove();
1332 var $content = $messageBody.children('div:eq(0)');
1333
1334 // insert wysiwyg
1335 $('' + data.returnValues.template).appendTo($content);
1336
1337 // bind buttons
1338 var $formSubmit = $content.find('.formSubmit');
1339 var $saveButton = $formSubmit.find('button[data-type=save]').click($.proxy(this._save, this));
1340 if (this._supportExtendedForm) $formSubmit.find('button[data-type=extended]').click($.proxy(this._prepareExtended, this));
1341 $formSubmit.find('button[data-type=cancel]').click($.proxy(this._cancel, this));
1342
1343 WCF.Message.Submit.registerButton(
1344 this._messageEditorIDPrefix + this._container[this._activeElementID].data('objectID'),
1345 $saveButton
1346 );
1347
1348 // hide message options
1349 this._container[this._activeElementID].find('.messageOptions').addClass('forceHidden');
1350
1351 if ($.browser.ckeditor) {
1352 new WCF.PeriodicalExecuter($.proxy(function(pe) {
1353 pe.stop();
1354
1355 var $ckEditor = $('#' + this._messageEditorIDPrefix + this._container[this._activeElementID].data('objectID'));
1356 $ckEditor.ckeditor(function() { this.ui.editor.focus(); });
1357
1358 if (this._quoteManager) {
1359 this._quoteManager.setAlternativeCKEditor($ckEditor);
1360 }
1361 }, this), 250);
1362 }
1363 else {
1364 $('#' + this._messageEditorIDPrefix + this._container[this._activeElementID].data('objectID')).focus();
1365 }
1366 },
1367
1368 /**
1369 * Reverts editor.
1370 */
1371 _revertEditor: function() {
1372 var $messageBody = this._container[this._activeElementID].removeClass('jsInvalidQuoteTarget').find('.messageBody');
1373 $messageBody.children('span.icon-spinner').remove();
1374 $messageBody.children('div:eq(0)').children().show();
1375
1376 // show unrelated content
1377 $messageBody.find('.jsInlineEditorHideContent').show();
1378
1379 if (this._quoteManager) {
1380 this._quoteManager.clearAlternativeCKEditor();
1381 }
1382 },
1383
1384 /**
1385 * Saves editor contents.
1386 */
1387 _save: function() {
1388 var $container = this._container[this._activeElementID];
1389 var $objectID = $container.data('objectID');
1390 var $message = '';
1391
1392 if ($.browser.ckeditor) {
1393 var $ckEditor = $('#' + this._messageEditorIDPrefix + $objectID).ckeditorGet();
1394 $message = $ckEditor.getData();
1395 }
1396 else {
1397 $message = $('#' + this._messageEditorIDPrefix + $objectID).val();
1398 }
1399
1400 this._proxy.setOption('data', {
1401 actionName: 'save',
1402 className: this._getClassName(),
1403 interfaceName: 'wcf\\data\\IMessageInlineEditorAction',
1404 parameters: {
1405 containerID: this._containerID,
1406 data: {
1407 message: $message
1408 },
1409 objectID: $objectID
1410 }
1411 });
1412 this._proxy.sendRequest();
1413
1414 this._hideEditor();
1415 },
1416
1417 /**
1418 * Prepares jumping to extended editing mode.
1419 */
1420 _prepareExtended: function() {
1421 var $container = this._container[this._activeElementID];
1422 var $objectID = $container.data('objectID');
1423 var $message = '';
1424
1425 if ($.browser.ckeditor) {
1426 var $ckEditor = $('#' + this._messageEditorIDPrefix + $objectID).ckeditorGet();
1427 $message = $ckEditor.getData();
1428 }
1429 else {
1430 $message = $('#' + this._messageEditorIDPrefix + $objectID).val();
1431 }
1432
1433 new WCF.Action.Proxy({
1434 autoSend: true,
1435 data: {
1436 actionName: 'jumpToExtended',
1437 className: this._getClassName(),
1438 parameters: {
1439 containerID: this._containerID,
1440 message: $message,
1441 messageID: $objectID
1442 }
1443 },
1444 success: function(data, textStatus, jqXHR) {
1445 window.location = data.returnValues.url;
1446 }
1447 });
1448 },
1449
1450 /**
1451 * Hides WYSIWYG editor.
1452 */
1453 _hideEditor: function() {
1454 var $messageBody = this._container[this._activeElementID].removeClass('jsInvalidQuoteTarget').find('.messageBody');
1455 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody);
1456 $messageBody.children('div:eq(0)').children().hide();
1457
1458 // show unrelated content
1459 $messageBody.find('.jsInlineEditorHideContent').show();
1460
1461 if (this._quoteManager) {
1462 this._quoteManager.clearAlternativeCKEditor();
1463 }
1464 },
1465
1466 /**
1467 * Shows rendered message.
1468 *
1469 * @param object data
1470 */
1471 _showMessage: function(data) {
1472 var $container = this._container[this._activeElementID].removeClass('jsInvalidQuoteTarget');
1473 var $messageBody = $container.find('.messageBody');
1474 $messageBody.children('.icon-spinner').remove();
1475 var $content = $messageBody.children('div:eq(0)');
1476
1477 // show unrelated content
1478 $content.parent().children('.jsInlineEditorHideContent').show();
1479
1480 // revert message options
1481 this._container[this._activeElementID].find('.messageOptions').removeClass('forceHidden');
1482
1483 // remove editor
1484 if ($.browser.ckeditor) {
1485 var $ckEditor = $('#' + this._messageEditorIDPrefix + $container.data('objectID')).ckeditorGet();
1486 $ckEditor.destroy();
1487 }
1488
1489 $content.empty();
1490
1491 // insert new message
1492 $content.html('<div class="messageText">' + data.returnValues.message + '</div>');
1493
1494 this._activeElementID = '';
1495
1496 this._updateHistory(this._getHash($container.data('objectID')));
1497
1498 this._notification.show();
1499
1500 if (this._quoteManager) {
1501 this._quoteManager.clearAlternativeCKEditor();
1502 }
1503 },
1504
1505 /**
1506 * Returns message action class name.
1507 *
1508 * @return string
1509 */
1510 _getClassName: function() {
1511 return '';
1512 },
1513
1514 /**
1515 * Returns the hash added to the url after successfully editing a message.
1516 *
1517 * @return string
1518 */
1519 _getHash: function(objectID) {
1520 return '#message' + objectID;
1521 },
1522
1523 /**
1524 * Updates the history to avoid old content when going back in the browser
1525 * history.
1526 *
1527 * @param hash
1528 */
1529 _updateHistory: function(hash) {
1530 window.location.hash = hash;
1531 }
1532 });
1533
1534 /**
1535 * Handles submit buttons for forms with an embedded WYSIWYG editor.
1536 */
1537 WCF.Message.Submit = {
1538 /**
1539 * list of registered buttons
1540 * @var object
1541 */
1542 _buttons: { },
1543
1544 /**
1545 * Registers submit button for specified wysiwyg container id.
1546 *
1547 * @param string wysiwygContainerID
1548 * @param string selector
1549 */
1550 registerButton: function(wysiwygContainerID, selector) {
1551 if (!WCF.Browser.isChrome()) {
1552 return;
1553 }
1554
1555 this._buttons[wysiwygContainerID] = $(selector);
1556 },
1557
1558 /**
1559 * Triggers 'click' event for registered buttons.
1560 */
1561 execute: function(wysiwygContainerID) {
1562 if (!this._buttons[wysiwygContainerID]) {
1563 return;
1564 }
1565
1566 this._buttons[wysiwygContainerID].trigger('click');
1567 }
1568 };
1569
1570 /**
1571 * Namespace for message quotes.
1572 */
1573 WCF.Message.Quote = { };
1574
1575 /**
1576 * Handles message quotes.
1577 *
1578 * @param string className
1579 * @param string objectType
1580 * @param string containerSelector
1581 * @param string messageBodySelector
1582 */
1583 WCF.Message.Quote.Handler = Class.extend({
1584 /**
1585 * active container id
1586 * @var string
1587 */
1588 _activeContainerID: '',
1589
1590 /**
1591 * action class name
1592 * @var string
1593 */
1594 _className: '',
1595
1596 /**
1597 * list of message containers
1598 * @var object
1599 */
1600 _containers: { },
1601
1602 /**
1603 * container selector
1604 * @var string
1605 */
1606 _containerSelector: '',
1607
1608 /**
1609 * 'copy quote' overlay
1610 * @var jQuery
1611 */
1612 _copyQuote: null,
1613
1614 /**
1615 * marked message
1616 * @var string
1617 */
1618 _message: '',
1619
1620 /**
1621 * message body selector
1622 * @var string
1623 */
1624 _messageBodySelector: '',
1625
1626 /**
1627 * object id
1628 * @var integer
1629 */
1630 _objectID: 0,
1631
1632 /**
1633 * object type name
1634 * @var string
1635 */
1636 _objectType: '',
1637
1638 /**
1639 * action proxy
1640 * @var WCF.Action.Proxy
1641 */
1642 _proxy: null,
1643
1644 /**
1645 * quote manager
1646 * @var WCF.Message.Quote.Manager
1647 */
1648 _quoteManager: null,
1649
1650 /**
1651 * Initializes the quote handler for given object type.
1652 *
1653 * @param WCF.Message.Quote.Manager quoteManager
1654 * @param string className
1655 * @param string objectType
1656 * @param string containerSelector
1657 * @param string messageBodySelector
1658 * @param string messageContentSelector
1659 */
1660 init: function(quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector) {
1661 this._className = className;
1662 if (this._className == '') {
1663 console.debug("[WCF.Message.QuoteManager] Empty class name given, aborting.");
1664 return;
1665 }
1666
1667 this._objectType = objectType;
1668 if (this._objectType == '') {
1669 console.debug("[WCF.Message.QuoteManager] Empty object type name given, aborting.");
1670 return;
1671 }
1672
1673 this._containerSelector = containerSelector;
1674 this._message = '';
1675 this._messageBodySelector = messageBodySelector;
1676 this._messageContentSelector = messageContentSelector;
1677 this._objectID = 0;
1678 this._proxy = new WCF.Action.Proxy({
1679 success: $.proxy(this._success, this)
1680 });
1681
1682 this._initContainers();
1683 this._initCopyQuote();
1684
1685 $(document).mouseup($.proxy(this._mouseUp, this));
1686
1687 // register with quote manager
1688 this._quoteManager = quoteManager;
1689 this._quoteManager.register(this._objectType, this);
1690
1691 // register with DOMNodeInsertedHandler
1692 WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.Quote.Handler' + objectType.hashCode(), $.proxy(this._initContainers, this));
1693 },
1694
1695 /**
1696 * Initializes message containers.
1697 */
1698 _initContainers: function() {
1699 var self = this;
1700 $(this._containerSelector).each(function(index, container) {
1701 var $container = $(container);
1702 var $containerID = $container.wcfIdentify();
1703
1704 if (!self._containers[$containerID]) {
1705 self._containers[$containerID] = $container;
1706 if ($container.hasClass('jsInvalidQuoteTarget')) {
1707 return true;
1708 }
1709
1710 if (self._messageBodySelector !== null) {
1711 $container = $container.find(self._messageBodySelector).data('containerID', $containerID);
1712 }
1713
1714 $container.mousedown($.proxy(self._mouseDown, self));
1715
1716 // bind event to quote whole message
1717 self._containers[$containerID].find('.jsQuoteMessage').click($.proxy(self._saveFullQuote, self));
1718 }
1719 });
1720 },
1721
1722 /**
1723 * Handles mouse down event.
1724 *
1725 * @param object event
1726 */
1727 _mouseDown: function(event) {
1728 // hide copy quote
1729 this._copyQuote.hide();
1730
1731 // store container ID
1732 var $container = $(event.currentTarget);
1733
1734 if (this._messageBodySelector) {
1735 $container = this._containers[$container.data('containerID')];
1736 }
1737
1738 if ($container.hasClass('jsInvalidQuoteTarget')) {
1739 this._activeContainerID = '';
1740
1741 return;
1742 }
1743
1744 this._activeContainerID = $container.wcfIdentify();
1745
1746 // remove alt-tag from all images, fixes quoting in Firefox
1747 if ($.browser.mozilla) {
1748 $container.find('img').each(function() {
1749 var $image = $(this);
1750 $image.data('__alt', $image.attr('alt')).removeAttr('alt');
1751 });
1752 }
1753 },
1754
1755 /**
1756 * Returns the text of a node and its children.
1757 *
1758 * @param object node
1759 * @return string
1760 */
1761 _getNodeText: function(node) {
1762 var nodeText = '';
1763
1764 for (var i = 0; i < node.childNodes.length; i++) {
1765 if (node.childNodes[i].nodeType == 3) {
1766 // text node
1767 nodeText += node.childNodes[i].nodeValue;
1768 }
1769 else {
1770 if (!node.childNodes[i].tagName) {
1771 continue;
1772 }
1773
1774 var $tagName = node.childNodes[i].tagName.toLowerCase();
1775 if ($tagName === 'li') {
1776 nodeText += "\r\n";
1777 }
1778 else if ($tagName === 'td' && !$.browser.msie) {
1779 nodeText += "\r\n";
1780 }
1781
1782 nodeText += this._getNodeText(node.childNodes[i]);
1783
1784 if ($tagName === 'ul') {
1785 nodeText += "\n";
1786 }
1787 }
1788 }
1789
1790 return nodeText;
1791 },
1792
1793 /**
1794 * Handles the mouse up event.
1795 *
1796 * @param object event
1797 */
1798 _mouseUp: function(event) {
1799 // ignore event
1800 if (this._activeContainerID == '') {
1801 this._copyQuote.hide();
1802
1803 return;
1804 }
1805
1806 var $container = this._containers[this._activeContainerID];
1807 var $selection = this._getSelectedText();
1808 var $text = $.trim($selection);
1809 if ($text == '') {
1810 this._copyQuote.hide();
1811
1812 return;
1813 }
1814
1815 // compare selection with message text of given container
1816 var $messageText = null;
1817 if (this._messageBodySelector) {
1818 $messageText = this._getNodeText($container.find(this._messageContentSelector).get(0));
1819 }
1820 else {
1821 $messageText = this._getNodeText($container.get(0));
1822 }
1823
1824 // selected text is not part of $messageText or contains text from unrelated nodes
1825 if (this._normalize($messageText).indexOf(this._normalize($text)) === -1) {
1826 return;
1827 }
1828 this._copyQuote.show();
1829
1830 var $coordinates = this._getBoundingRectangle($container, $selection);
1831 var $dimensions = this._copyQuote.getDimensions('outer');
1832 var $left = ($coordinates.right - $coordinates.left) / 2 - ($dimensions.width / 2) + $coordinates.left;
1833
1834 this._copyQuote.css({
1835 top: $coordinates.top - $dimensions.height - 7 + 'px',
1836 left: $left + 'px'
1837 });
1838 this._copyQuote.hide();
1839
1840 // reset containerID
1841 this._activeContainerID = '';
1842
1843 // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
1844 var self = this;
1845 new WCF.PeriodicalExecuter(function(pe) {
1846 pe.stop();
1847
1848 var $text = $.trim(self._getSelectedText());
1849 if ($text != '') {
1850 self._copyQuote.show();
1851 self._message = $text;
1852 self._objectID = $container.data('objectID');
1853
1854 // revert alt tags, fixes quoting in Firefox
1855 if ($.browser.mozilla) {
1856 $container.find('img').each(function() {
1857 var $image = $(this);
1858 $image.attr('alt', $image.data('__alt'));
1859 });
1860 }
1861 }
1862 }, 10);
1863 },
1864
1865 /**
1866 * Normalizes a text for comparison.
1867 *
1868 * @param string text
1869 * @return string
1870 */
1871 _normalize: function(text) {
1872 return text.replace(/\r?\n|\r/g, "\n").replace(/\s/g, ' ').replace(/\s{2,}/g, ' ');
1873 },
1874
1875 /**
1876 * Returns the left or right offset of the current text selection.
1877 *
1878 * @param object range
1879 * @param boolean before
1880 * @return object
1881 */
1882 _getOffset: function(range, before) {
1883 range.collapse(before);
1884
1885 var $elementID = WCF.getRandomID();
1886 var $element = document.createElement('span');
1887 $element.innerHTML = '<span id="' + $elementID + '"></span>';
1888 var $fragment = document.createDocumentFragment(), $node;
1889 while ($node = $element.firstChild) {
1890 $fragment.appendChild($node);
1891 }
1892 range.insertNode($fragment);
1893
1894 $element = $('#' + $elementID);
1895 var $position = $element.offset();
1896 $position.top = $position.top - $(window).scrollTop();
1897 $element.remove();
1898
1899 return $position;
1900 },
1901
1902 /**
1903 * Returns the offsets of the selection's bounding rectangle.
1904 *
1905 * @return object
1906 */
1907 _getBoundingRectangle: function(container, selection) {
1908 var $coordinates = null;
1909
1910 if (document.createRange && typeof document.createRange().getBoundingClientRect != "undefined") { // Opera, Firefox, Safari, Chrome
1911 if (selection.rangeCount > 0) {
1912 // the coordinates returned by getBoundingClientRect() is relative to the window, not the document!
1913 //var $rect = selection.getRangeAt(0).getBoundingClientRect();
1914 var $rects = selection.getRangeAt(0).getClientRects();
1915 var $rect = selection.getRangeAt(0).getBoundingClientRect();
1916
1917 /*
1918 var $rect = { };
1919 if (!$.browser.mozilla && $rects.length > 1) {
1920 // save current selection to restore it later
1921 var $range = selection.getRangeAt(0);
1922 var $bckp = this._saveSelection(container.get(0));
1923 var $position1 = this._getOffset($range, true);
1924
1925 var $range = selection.getRangeAt(0);
1926 var $position2 = this._getOffset($range, false);
1927
1928 $rect = {
1929 left: Math.min($position1.left, $position2.left),
1930 right: Math.max($position1.left, $position2.left),
1931 top: Math.max($position1.top, $position2.top)
1932 };
1933
1934 // restore selection
1935 this._restoreSelection(container.get(0), $bckp);
1936 }
1937 else {
1938 $rect = selection.getRangeAt(0).getBoundingClientRect();
1939 }
1940 */
1941
1942 var $document = $(document);
1943 var $offsetTop = $document.scrollTop();
1944
1945 $coordinates = {
1946 left: $rect.left,
1947 right: $rect.right,
1948 top: $rect.top + $offsetTop
1949 };
1950 }
1951 }
1952 else if (document.selection && document.selection.type != "Control") { // IE
1953 var $range = document.selection.createRange();
1954
1955 $coordinates = {
1956 left: $range.boundingLeft,
1957 right: $range.boundingRight,
1958 top: $range.boundingTop
1959 };
1960 }
1961
1962 return $coordinates;
1963 },
1964
1965 /**
1966 * Saves current selection.
1967 *
1968 * @see http://stackoverflow.com/a/13950376
1969 *
1970 * @param object containerEl
1971 * @return object
1972 */
1973 _saveSelection: function(containerEl) {
1974 if (window.getSelection && document.createRange) {
1975 var range = window.getSelection().getRangeAt(0);
1976 var preSelectionRange = range.cloneRange();
1977 preSelectionRange.selectNodeContents(containerEl);
1978 preSelectionRange.setEnd(range.startContainer, range.startOffset);
1979 var start = preSelectionRange.toString().length;
1980
1981 return {
1982 start: start,
1983 end: start + range.toString().length
1984 };
1985 }
1986 else {
1987 var selectedTextRange = document.selection.createRange();
1988 var preSelectionTextRange = document.body.createTextRange();
1989 preSelectionTextRange.moveToElementText(containerEl);
1990 preSelectionTextRange.setEndPoint("EndToStart", selectedTextRange);
1991 var start = preSelectionTextRange.text.length;
1992
1993 return {
1994 start: start,
1995 end: start + selectedTextRange.text.length
1996 };
1997 }
1998 },
1999
2000 /**
2001 * Restores a selection.
2002 *
2003 * @see http://stackoverflow.com/a/13950376
2004 *
2005 * @param object containerEl
2006 * @param object savedSel
2007 */
2008 _restoreSelection: function(containerEl, savedSel) {
2009 if (window.getSelection && document.createRange) {
2010 var charIndex = 0, range = document.createRange();
2011 range.setStart(containerEl, 0);
2012 range.collapse(true);
2013 var nodeStack = [containerEl], node, foundStart = false, stop = false;
2014
2015 while (!stop && (node = nodeStack.pop())) {
2016 if (node.nodeType == 3) {
2017 var nextCharIndex = charIndex + node.length;
2018 if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
2019 range.setStart(node, savedSel.start - charIndex);
2020 foundStart = true;
2021 }
2022 if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
2023 range.setEnd(node, savedSel.end - charIndex);
2024 stop = true;
2025 }
2026 charIndex = nextCharIndex;
2027 } else {
2028 var i = node.childNodes.length;
2029 while (i--) {
2030 nodeStack.push(node.childNodes[i]);
2031 };
2032 };
2033 }
2034
2035 var sel = window.getSelection();
2036 sel.removeAllRanges();
2037 sel.addRange(range);
2038 }
2039 else {
2040 var textRange = document.body.createTextRange();
2041 textRange.moveToElementText(containerEl);
2042 textRange.collapse(true);
2043 textRange.moveEnd("character", savedSel.end);
2044 textRange.moveStart("character", savedSel.start);
2045 textRange.select();
2046 }
2047 },
2048
2049 /**
2050 * Initializes the 'copy quote' element.
2051 */
2052 _initCopyQuote: function() {
2053 this._copyQuote = $('#quoteManagerCopy');
2054 if (!this._copyQuote.length) {
2055 this._copyQuote = $('<div id="quoteManagerCopy" class="balloonTooltip"><span>' + WCF.Language.get('wcf.message.quote.quoteSelected') + '</span><span class="pointer"><span></span></span></div>').hide().appendTo(document.body);
2056 this._copyQuote.click($.proxy(this._saveQuote, this));
2057 }
2058 },
2059
2060 /**
2061 * Returns the text selection.
2062 *
2063 * @return object
2064 */
2065 _getSelectedText: function() {
2066 if (window.getSelection) { // Opera, Firefox, Safari, Chrome, IE 9+
2067 return window.getSelection();
2068 }
2069 else if (document.getSelection) { // Opera, Firefox, Safari, Chrome, IE 9+
2070 return document.getSelection();
2071 }
2072 else if (document.selection) { // IE 8
2073 return document.selection.createRange().text;
2074 }
2075
2076 return '';
2077 },
2078
2079 /**
2080 * Saves a full quote.
2081 *
2082 * @param object event
2083 */
2084 _saveFullQuote: function(event) {
2085 var $listItem = $(event.currentTarget);
2086
2087 this._proxy.setOption('data', {
2088 actionName: 'saveFullQuote',
2089 className: this._className,
2090 interfaceName: 'wcf\\data\\IMessageQuoteAction',
2091 objectIDs: [ $listItem.data('objectID') ]
2092 });
2093 this._proxy.sendRequest();
2094
2095 // mark element as quoted
2096 if ($listItem.data('isQuoted')) {
2097 $listItem.data('isQuoted', false).children('a').removeClass('active');
2098 }
2099 else {
2100 $listItem.data('isQuoted', true).children('a').addClass('active');
2101 }
2102
2103 // discard event
2104 event.stopPropagation();
2105 return false;
2106 },
2107
2108 /**
2109 * Saves a quote.
2110 */
2111 _saveQuote: function() {
2112 this._proxy.setOption('data', {
2113 actionName: 'saveQuote',
2114 className: this._className,
2115 interfaceName: 'wcf\\data\\IMessageQuoteAction',
2116 objectIDs: [ this._objectID ],
2117 parameters: {
2118 message: this._message
2119 }
2120 });
2121 this._proxy.sendRequest();
2122 },
2123
2124 /**
2125 * Handles successful AJAX requests.
2126 *
2127 * @param object data
2128 * @param string textStatus
2129 * @param jQuery jqXHR
2130 */
2131 _success: function(data, textStatus, jqXHR) {
2132 if (data.returnValues.count !== undefined) {
2133 var $fullQuoteObjectIDs = (data.fullQuoteObjectIDs !== undefined) ? data.fullQuoteObjectIDs : { };
2134 this._quoteManager.updateCount(data.returnValues.count, $fullQuoteObjectIDs);
2135 }
2136 },
2137
2138 /**
2139 * Updates the full quote data for all matching objects.
2140 *
2141 * @param array<integer> $objectIDs
2142 */
2143 updateFullQuoteObjectIDs: function(objectIDs) {
2144 for (var $containerID in this._containers) {
2145 this._containers[$containerID].find('.jsQuoteMessage').each(function(index, button) {
2146 // reset all markings
2147 var $button = $(button).data('isQuoted', 0);
2148 $button.children('a').removeClass('active');
2149
2150 // mark as active
2151 if (WCF.inArray($button.data('objectID'), objectIDs)) {
2152 $button.data('isQuoted', 1).children('a').addClass('active');
2153 }
2154 });
2155 }
2156 }
2157 });
2158
2159 /**
2160 * Manages stored quotes.
2161 *
2162 * @param integer count
2163 */
2164 WCF.Message.Quote.Manager = Class.extend({
2165 /**
2166 * list of form buttons
2167 * @var object
2168 */
2169 _buttons: { },
2170
2171 /**
2172 * CKEditor element
2173 * @var jQuery
2174 */
2175 _ckEditor: null,
2176
2177 /**
2178 * alternative CKEditor element
2179 * @var jQuery
2180 */
2181 _ckEditorAlternative: null,
2182
2183 /**
2184 * number of stored quotes
2185 * @var integer
2186 */
2187 _count: 0,
2188
2189 /**
2190 * dialog overlay
2191 * @var jQuery
2192 */
2193 _dialog: null,
2194
2195 /**
2196 * form element
2197 * @var jQuery
2198 */
2199 _form: null,
2200
2201 /**
2202 * list of quote handlers
2203 * @var object
2204 */
2205 _handlers: { },
2206
2207 /**
2208 * true, if an up-to-date template exists
2209 * @var boolean
2210 */
2211 _hasTemplate: false,
2212
2213 /**
2214 * true, if related quotes should be inserted
2215 * @var boolean
2216 */
2217 _insertQuotes: true,
2218
2219 /**
2220 * action proxy
2221 * @var WCF.Action.Proxy
2222 */
2223 _proxy: null,
2224
2225 /**
2226 * list of quotes to remove upon submit
2227 * @var array<string>
2228 */
2229 _removeOnSubmit: [ ],
2230
2231 /**
2232 * show quotes element
2233 * @var jQuery
2234 */
2235 _showQuotes: null,
2236
2237 /**
2238 * allow pasting
2239 * @var boolean
2240 */
2241 _supportPaste: false,
2242
2243 /**
2244 * Initializes the quote manager.
2245 *
2246 * @param integer count
2247 * @param string ckEditorID
2248 * @param boolean supportPaste
2249 * @param array<string> removeOnSubmit
2250 */
2251 init: function(count, ckEditorID, supportPaste, removeOnSubmit) {
2252 this._buttons = {
2253 insert: null,
2254 remove: null
2255 };
2256 this._ckEditor = null;
2257 this._ckEditorAlternative = null;
2258 this._count = parseInt(count) || 0;
2259 this._dialog = null;
2260 this._form = null;
2261 this._handlers = { };
2262 this._hasTemplate = false;
2263 this._insertQuotes = true;
2264 this._removeOnSubmit = [ ];
2265 this._showQuotes = null;
2266 this._supportPaste = false;
2267
2268 if (ckEditorID) {
2269 this._ckEditor = $('#' + ckEditorID);
2270 if (this._ckEditor.length) {
2271 this._supportPaste = true;
2272
2273 // get surrounding form-tag
2274 this._form = this._ckEditor.parents('form:eq(0)');
2275 if (this._form.length) {
2276 this._form.submit($.proxy(this._submit, this));
2277 this._removeOnSubmit = removeOnSubmit || [ ];
2278 }
2279 else {
2280 this._form = null;
2281
2282 // allow override
2283 this._supportPaste = (supportPaste === true) ? true : false;
2284 }
2285 }
2286 }
2287
2288 this._proxy = new WCF.Action.Proxy({
2289 showLoadingOverlay: false,
2290 success: $.proxy(this._success, this),
2291 url: 'index.php/MessageQuote/?t=' + SECURITY_TOKEN + SID_ARG_2ND
2292 });
2293
2294 this._toggleShowQuotes();
2295 },
2296
2297 /**
2298 * Sets an alternative CKEditor instance on runtime.
2299 *
2300 * @param jQuery ckEditor
2301 */
2302 setAlternativeCKEditor: function(ckEditor) {
2303 this._ckEditorAlternative = ckEditor;
2304 },
2305
2306 /**
2307 * Clears alternative CKEditor instance.
2308 */
2309 clearAlternativeCKEditor: function() {
2310 this._ckEditorAlternative = null;
2311 },
2312
2313 /**
2314 * Registers a quote handler.
2315 *
2316 * @param string objectType
2317 * @param WCF.Message.Quote.Handler handler
2318 */
2319 register: function(objectType, handler) {
2320 this._handlers[objectType] = handler;
2321 },
2322
2323 /**
2324 * Updates number of stored quotes.
2325 *
2326 * @param integer count
2327 * @param object fullQuoteObjectIDs
2328 */
2329 updateCount: function(count, fullQuoteObjectIDs) {
2330 this._count = parseInt(count) || 0;
2331
2332 this._toggleShowQuotes();
2333
2334 // update full quote ids of handlers
2335 for (var $objectType in this._handlers) {
2336 if (fullQuoteObjectIDs[$objectType]) {
2337 this._handlers[$objectType].updateFullQuoteObjectIDs(fullQuoteObjectIDs[$objectType]);
2338 }
2339 }
2340 },
2341
2342 /**
2343 * Inserts all associated quotes upon first time using quick reply.
2344 *
2345 * @param string className
2346 * @param integer parentObjectID
2347 * @param object callback
2348 */
2349 insertQuotes: function(className, parentObjectID, callback) {
2350 if (!this._insertQuotes) {
2351 this._insertQuotes = true;
2352
2353 return;
2354 }
2355
2356 new WCF.Action.Proxy({
2357 autoSend: true,
2358 data: {
2359 actionName: 'getRenderedQuotes',
2360 className: className,
2361 interfaceName: 'wcf\\data\\IMessageQuoteAction',
2362 parameters: {
2363 parentObjectID: parentObjectID
2364 }
2365 },
2366 success: callback
2367 });
2368 },
2369
2370 /**
2371 * Toggles the display of the 'Show quotes' button
2372 */
2373 _toggleShowQuotes: function() {
2374 if (!this._count) {
2375 if (this._showQuotes !== null) {
2376 this._showQuotes.hide();
2377 }
2378 }
2379 else {
2380 if (this._showQuotes === null) {
2381 this._showQuotes = $('#showQuotes');
2382 if (!this._showQuotes.length) {
2383 this._showQuotes = $('<div id="showQuotes" class="balloonTooltip" />').click($.proxy(this._click, this)).appendTo(document.body);
2384 }
2385 }
2386
2387 var $text = WCF.Language.get('wcf.message.quote.showQuotes').replace(/#count#/, this._count);
2388 this._showQuotes.text($text).show();
2389 }
2390
2391 this._hasTemplate = false;
2392 },
2393
2394 /**
2395 * Handles clicks on 'Show quotes'.
2396 */
2397 _click: function() {
2398 if (this._hasTemplate) {
2399 this._dialog.wcfDialog('open');
2400 }
2401 else {
2402 this._proxy.showLoadingOverlayOnce();
2403
2404 this._proxy.setOption('data', {
2405 actionName: 'getQuotes',
2406 supportPaste: this._supportPaste
2407 });
2408 this._proxy.sendRequest();
2409 }
2410 },
2411
2412 /**
2413 * Renders the dialog.
2414 *
2415 * @param string template
2416 */
2417 renderDialog: function(template) {
2418 // create dialog if not exists
2419 if (this._dialog === null) {
2420 this._dialog = $('#messageQuoteList');
2421 if (!this._dialog.length) {
2422 this._dialog = $('<div id="messageQuoteList" />').hide().appendTo(document.body);
2423 }
2424 }
2425
2426 // add template
2427 this._dialog.html(template);
2428
2429 // add 'insert' and 'delete' buttons
2430 var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog);
2431 if (this._supportPaste) this._buttons.insert = $('<button class="buttonPrimary">' + WCF.Language.get('wcf.message.quote.insertAllQuotes') + '</button>').click($.proxy(this._insertSelected, this)).appendTo($formSubmit);
2432 this._buttons.remove = $('<button>' + WCF.Language.get('wcf.message.quote.removeAllQuotes') + '</button>').click($.proxy(this._removeSelected, this)).appendTo($formSubmit);
2433
2434 // show dialog
2435 this._dialog.wcfDialog({
2436 title: WCF.Language.get('wcf.message.quote.manageQuotes')
2437 });
2438 this._dialog.wcfDialog('render');
2439 this._hasTemplate = true;
2440
2441 // bind event listener
2442 var $insertQuoteButtons = this._dialog.find('.jsInsertQuote');
2443 if (this._supportPaste) {
2444 $insertQuoteButtons.click($.proxy(this._insertQuote, this));
2445 }
2446 else {
2447 $insertQuoteButtons.hide();
2448 }
2449
2450 this._dialog.find('input.jsCheckbox').change($.proxy(this._changeButtons, this));
2451
2452 // mark quotes for removal
2453 if (this._removeOnSubmit.length) {
2454 var self = this;
2455 this._dialog.find('input.jsRemoveQuote').each(function(index, input) {
2456 var $input = $(input).change($.proxy(this._change, this));
2457
2458 // mark for deletion
2459 if (WCF.inArray($input.parent('li').attr('data-quote-id'), self._removeOnSubmit)) {
2460 $input.attr('checked', 'checked');
2461 }
2462 });
2463 }
2464 },
2465
2466 /**
2467 * Updates button labels if a checkbox is checked or unchecked.
2468 */
2469 _changeButtons: function() {
2470 // selection
2471 if (this._dialog.find('input.jsCheckbox:checked').length) {
2472 if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertSelectedQuotes'));
2473 this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeSelectedQuotes'));
2474 }
2475 else {
2476 // no selection, pick all
2477 if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertAllQuotes'));
2478 this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeAllQuotes'));
2479 }
2480 },
2481
2482 /**
2483 * Checks for change event on delete-checkboxes.
2484 *
2485 * @param object event
2486 */
2487 _change: function(event) {
2488 var $input = $(event.currentTarget);
2489 var $quoteID = $input.parent('li').attr('data-quote-id');
2490
2491 if ($input.prop('checked')) {
2492 this._removeOnSubmit.push($quoteID);
2493 }
2494 else {
2495 for (var $index in this._removeOnSubmit) {
2496 if (this._removeOnSubmit[$index] == $quoteID) {
2497 delete this._removeOnSubmit[$index];
2498 break;
2499 }
2500 }
2501 }
2502 },
2503
2504 /**
2505 * Inserts the selected quotes.
2506 */
2507 _insertSelected: function() {
2508 if (this._ckEditorAlternative === null) {
2509 var $api = $('.jsQuickReply:eq(0)').data('__api');
2510 if ($api && !$api.getContainer().is(':visible')) {
2511 this._insertQuotes = false;
2512 $api.click(null);
2513 }
2514 }
2515
2516 if (!this._dialog.find('input.jsCheckbox:checked').length) {
2517 this._dialog.find('input.jsCheckbox').prop('checked', 'checked');
2518 }
2519
2520 // insert all quotes
2521 this._dialog.find('input.jsCheckbox:checked').each($.proxy(function(index, input) {
2522 this._insertQuote(null, input);
2523 }, this));
2524
2525 // close dialog
2526 this._dialog.wcfDialog('close');
2527 },
2528
2529 /**
2530 * Inserts a quote.
2531 *
2532 * @param object event
2533 * @param object inputElement
2534 */
2535 _insertQuote: function(event, inputElement) {
2536 if (event !== null && this._ckEditorAlternative === null) {
2537 var $api = $('.jsQuickReply:eq(0)').data('__api');
2538 if ($api && !$api.getContainer().is(':visible')) {
2539 this._insertQuotes = false;
2540 $api.click(null);
2541 }
2542 }
2543
2544 var $listItem = (event === null) ? $(inputElement).parents('li') : $(event.currentTarget).parents('li');
2545 var $quote = $.trim($listItem.children('div.jsFullQuote').text());
2546 var $message = $listItem.parents('article.message');
2547
2548 // build quote tag
2549 $quote = "[quote='" + $message.attr('data-username') + "','" + $message.data('link') + "']" + $quote + "[/quote]";
2550
2551 // insert into ckEditor
2552 var $ckEditor = null;
2553 if ($.browser.ckeditor) {
2554 if (this._ckEditorAlternative === null) {
2555 $ckEditor = this._ckEditor.ckeditorGet();
2556 }
2557 else {
2558 $ckEditor = this._ckEditorAlternative.ckeditorGet();
2559 }
2560 }
2561
2562 if ($ckEditor !== null && $ckEditor.mode === 'wysiwyg') {
2563 // in design mode
2564
2565 // remove the link if the cursor is in a link element
2566 $ckEditor.removeStyle(new CKEDITOR.style({
2567 element: 'a',
2568 type: CKEDITOR.STYLE_INLINE
2569 }));
2570
2571 $ckEditor.insertText($quote + "\n\n");
2572 }
2573 else {
2574 // in source mode
2575 var $textarea = null;
2576 if (this._ckEditorAlternative === null) {
2577 $textarea = ($.browser.ckeditor) ? this._ckEditor.next('.cke_editor_text').find('textarea') : this._ckEditor;
2578 }
2579 else {
2580 $textarea = ($.browser.ckeditor) ? this._ckEditorAlternative.next('.cke_editor_text').find('textarea') : this._ckEditorAlternative;
2581 }
2582
2583 var $value = $textarea.val();
2584 $quote += "\n\n";
2585 if ($value.length == 0) {
2586 $textarea.val($quote);
2587 }
2588 else {
2589 var $position = $textarea.getCaret();
2590 $textarea.val( $value.substr(0, $position) + $quote + $value.substr($position) );
2591 }
2592 }
2593
2594 // remove quote upon submit or upon request
2595 this._removeOnSubmit.push($listItem.attr('data-quote-id'));
2596
2597 // close dialog
2598 if (event !== null) {
2599 this._dialog.wcfDialog('close');
2600 }
2601 },
2602
2603 /**
2604 * Removes selected quotes.
2605 */
2606 _removeSelected: function() {
2607 if (!this._dialog.find('input.jsCheckbox:checked').length) {
2608 this._dialog.find('input.jsCheckbox').prop('checked', 'checked');
2609 }
2610
2611 var $quoteIDs = [ ];
2612 this._dialog.find('input.jsCheckbox:checked').each(function(index, input) {
2613 $quoteIDs.push($(input).parents('li').attr('data-quote-id'));
2614 });
2615
2616 if ($quoteIDs.length) {
2617 // get object types
2618 var $objectTypes = [ ];
2619 for (var $objectType in this._handlers) {
2620 $objectTypes.push($objectType);
2621 }
2622
2623 this._proxy.setOption('data', {
2624 actionName: 'remove',
2625 getFullQuoteObjectIDs: this._handlers.length > 0,
2626 objectTypes: $objectTypes,
2627 quoteIDs: $quoteIDs
2628 });
2629 this._proxy.sendRequest();
2630
2631 this._dialog.wcfDialog('close');
2632 }
2633 },
2634
2635 /**
2636 * Appends list of quote ids to remove after successful submit.
2637 */
2638 _submit: function() {
2639 if (this._supportPaste && this._removeOnSubmit.length > 0) {
2640 var $formSubmit = this._form.find('.formSubmit');
2641 for (var $i in this._removeOnSubmit) {
2642 $('<input type="hidden" name="__removeQuoteIDs[]" value="' + this._removeOnSubmit[$i] + '" />').appendTo($formSubmit);
2643 }
2644 }
2645 },
2646
2647 /**
2648 * Returns a list of quote ids marked for removal.
2649 *
2650 * @return array<integer>
2651 */
2652 getQuotesMarkedForRemoval: function() {
2653 return this._removeOnSubmit;
2654 },
2655
2656 /**
2657 * Marks quote ids for removal.
2658 */
2659 markQuotesForRemoval: function() {
2660 if (this._removeOnSubmit.length) {
2661 this._proxy.setOption('data', {
2662 actionName: 'markForRemoval',
2663 quoteIDs: this._removeOnSubmit
2664 });
2665 this._proxy.suppressErrors();
2666 this._proxy.sendRequest();
2667 }
2668 },
2669
2670 /**
2671 * Removes all marked quote ids.
2672 */
2673 removeMarkedQuotes: function() {
2674 if (this._removeOnSubmit.length) {
2675 this._proxy.setOption('data', {
2676 actionName: 'removeMarkedQuotes',
2677 getFullQuoteObjectIDs: this._handlers.length > 0
2678 });
2679 this._proxy.sendRequest();
2680 }
2681 },
2682
2683 /**
2684 * Counts stored quotes.
2685 */
2686 countQuotes: function() {
2687 var $objectTypes = [ ];
2688 for (var $objectType in this._handlers) {
2689 $objectTypes.push($objectType);
2690 }
2691
2692 this._proxy.setOption('data', {
2693 actionName: 'count',
2694 getFullQuoteObjectIDs: this._handlers.length > 0,
2695 objectTypes: $objectTypes
2696 });
2697 this._proxy.sendRequest();
2698 },
2699
2700 /**
2701 * Handles successful AJAX requests.
2702 *
2703 * @param object data
2704 * @param string textStatus
2705 * @param jQuery jqXHR
2706 */
2707 _success: function(data, textStatus, jqXHR) {
2708 if (data === null) {
2709 return;
2710 }
2711
2712 if (data.count !== undefined) {
2713 var $fullQuoteObjectIDs = (data.fullQuoteObjectIDs !== undefined) ? data.fullQuoteObjectIDs : { };
2714 this.updateCount(data.count, $fullQuoteObjectIDs);
2715 }
2716
2717 if (data.template !== undefined) {
2718 if ($.trim(data.template) == '') {
2719 this.updateCount(0, { });
2720 }
2721 else {
2722 this.renderDialog(data.template);
2723 }
2724 }
2725 }
2726 });
2727
2728 /**
2729 * Namespace for message sharing related classes.
2730 */
2731 WCF.Message.Share = { };
2732
2733 /**
2734 * Displays a dialog overlay for permalinks.
2735 */
2736 WCF.Message.Share.Content = Class.extend({
2737 /**
2738 * list of cached templates
2739 * @var object
2740 */
2741 _cache: { },
2742
2743 /**
2744 * dialog overlay
2745 * @var jQuery
2746 */
2747 _dialog: null,
2748
2749 /**
2750 * Initializes the WCF.Message.Share.Content class.
2751 */
2752 init: function() {
2753 this._cache = { };
2754 this._dialog = null;
2755
2756 this._initLinks();
2757
2758 WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.Share.Content', $.proxy(this._initLinks, this));
2759 },
2760
2761 /**
2762 * Initializes share links.
2763 */
2764 _initLinks: function() {
2765 $('a.jsButtonShare').removeClass('jsButtonShare').click($.proxy(this._click, this));
2766 },
2767
2768 /**
2769 * Displays links to share this content.
2770 *
2771 * @param object event
2772 */
2773 _click: function(event) {
2774 event.preventDefault();
2775
2776 var $target = $(event.currentTarget);
2777 var $link = $target.prop('href');
2778 var $title = ($target.data('linkTitle') ? $target.data('linkTitle') : $link);
2779 var $key = $link.hashCode();
2780 if (this._cache[$key] === undefined) {
2781 // remove dialog contents
2782 var $dialogInitialized = false;
2783 if (this._dialog === null) {
2784 this._dialog = $('<div />').hide().appendTo(document.body);
2785 $dialogInitialized = true;
2786 }
2787 else {
2788 this._dialog.empty();
2789 }
2790
2791 // permalink (plain text)
2792 var $fieldset = $('<fieldset><legend><label for="__sharePermalink">' + WCF.Language.get('wcf.message.share.permalink') + '</label></legend></fieldset>').appendTo(this._dialog);
2793 $('<input type="text" id="__sharePermalink" class="long" readonly="readonly" />').attr('value', $link).appendTo($fieldset);
2794
2795 // permalink (BBCode)
2796 var $fieldset = $('<fieldset><legend><label for="__sharePermalinkBBCode">' + WCF.Language.get('wcf.message.share.permalink.bbcode') + '</label></legend></fieldset>').appendTo(this._dialog);
2797 $('<input type="text" id="__sharePermalinkBBCode" class="long" readonly="readonly" />').attr('value', '[url=\'' + $link + '\']' + $title + '[/url]').appendTo($fieldset);
2798
2799 // permalink (HTML)
2800 var $fieldset = $('<fieldset><legend><label for="__sharePermalinkHTML">' + WCF.Language.get('wcf.message.share.permalink.html') + '</label></legend></fieldset>').appendTo(this._dialog);
2801 $('<input type="text" id="__sharePermalinkHTML" class="long" readonly="readonly" />').attr('value', '<a href="' + $link + '">' + WCF.String.escapeHTML($title) + '</a>').appendTo($fieldset);
2802
2803 this._cache[$key] = this._dialog.html();
2804
2805 if ($dialogInitialized) {
2806 this._dialog.wcfDialog({
2807 title: WCF.Language.get('wcf.message.share')
2808 });
2809 }
2810 else {
2811 this._dialog.wcfDialog('open');
2812 }
2813 }
2814 else {
2815 this._dialog.html(this._cache[$key]).wcfDialog('open');
2816 }
2817
2818 this._enableSelection();
2819 },
2820
2821 /**
2822 * Enables text selection.
2823 */
2824 _enableSelection: function() {
2825 var $inputElements = this._dialog.find('input').click(function() { $(this).select(); });
2826
2827 // Safari on iOS can only select the text if it is not readonly and setSelectionRange() is used
2828 if (navigator.userAgent.match(/iP(ad|hone|od)/)) {
2829 $inputElements.keydown(function() { return false; }).removeAttr('readonly').click(function() { this.setSelectionRange(0, 9999); });
2830 }
2831 }
2832 });
2833
2834 /**
2835 * Provides buttons to share a page through multiple social community sites.
2836 *
2837 * @param boolean fetchObjectCount
2838 */
2839 WCF.Message.Share.Page = Class.extend({
2840 /**
2841 * list of share buttons
2842 * @var object
2843 */
2844 _ui: { },
2845
2846 /**
2847 * page description
2848 * @var string
2849 */
2850 _pageDescription: '',
2851
2852 /**
2853 * canonical page URL
2854 * @var string
2855 */
2856 _pageURL: '',
2857
2858 /**
2859 * Initializes the WCF.Message.Share.Page class.
2860 *
2861 * @param boolean fetchObjectCount
2862 */
2863 init: function(fetchObjectCount) {
2864 this._pageDescription = encodeURIComponent($('meta[property="og:title"]').prop('content'));
2865 this._pageURL = encodeURIComponent($('meta[property="og:url"]').prop('content'));
2866
2867 var $container = $('.messageShareButtons');
2868 this._ui = {
2869 facebook: $container.find('.jsShareFacebook'),
2870 google: $container.find('.jsShareGoogle'),
2871 reddit: $container.find('.jsShareReddit'),
2872 twitter: $container.find('.jsShareTwitter')
2873 };
2874
2875 this._ui.facebook.children('a').click($.proxy(this._shareFacebook, this));
2876 this._ui.google.children('a').click($.proxy(this._shareGoogle, this));
2877 this._ui.reddit.children('a').click($.proxy(this._shareReddit, this));
2878 this._ui.twitter.children('a').click($.proxy(this._shareTwitter, this));
2879
2880 if (fetchObjectCount === true) {
2881 this._fetchFacebook();
2882 this._fetchTwitter();
2883 this._fetchReddit();
2884 }
2885 },
2886
2887 /**
2888 * Shares current page to selected social community site.
2889 *
2890 * @param string objectName
2891 * @param string url
2892 * @param boolean appendURL
2893 */
2894 _share: function(objectName, url, appendURL) {
2895 window.open(url.replace(/{pageURL}/, this._pageURL).replace(/{text}/, this._pageDescription + (appendURL ? " " + this._pageURL : "")), 'height=600,width=600');
2896 },
2897
2898 /**
2899 * Shares current page with Facebook.
2900 */
2901 _shareFacebook: function() {
2902 this._share('facebook', 'https://www.facebook.com/sharer.php?u={pageURL}&t={text}', true);
2903 },
2904
2905 /**
2906 * Shares current page with Google Plus.
2907 */
2908 _shareGoogle: function() {
2909 this._share('google', 'https://plus.google.com/share?url={pageURL}', true);
2910 },
2911
2912 /**
2913 * Shares current page with Reddit.
2914 */
2915 _shareReddit: function() {
2916 this._share('reddit', 'https://ssl.reddit.com/submit?url={pageURL}', true);
2917 },
2918
2919 /**
2920 * Shares current page with Twitter.
2921 */
2922 _shareTwitter: function() {
2923 this._share('twitter', 'https://twitter.com/share?url={pageURL}&text={text}', false);
2924 },
2925
2926 /**
2927 * Fetches share count from a social community site.
2928 *
2929 * @param string url
2930 * @param object callback
2931 * @param string callbackName
2932 */
2933 _fetchCount: function(url, callback, callbackName) {
2934 var $options = {
2935 autoSend: true,
2936 dataType: 'jsonp',
2937 showLoadingOverlay: false,
2938 success: callback,
2939 suppressErrors: true,
2940 type: 'GET',
2941 url: url.replace(/{pageURL}/, this._pageURL)
2942 };
2943 if (callbackName) {
2944 $options.jsonp = callbackName;
2945 }
2946
2947 new WCF.Action.Proxy($options);
2948 },
2949
2950 /**
2951 * Fetches number of Facebook likes.
2952 */
2953 _fetchFacebook: function() {
2954 this._fetchCount('https://graph.facebook.com/?id={pageURL}', $.proxy(function(data) {
2955 if (data.shares) {
2956 this._ui.facebook.children('span.badge').show().text(data.shares);
2957 }
2958 }, this));
2959 },
2960
2961 /**
2962 * Fetches tweet count from Twitter.
2963 */
2964 _fetchTwitter: function() {
2965 if (window.location.protocol.match(/^https/)) return;
2966
2967 this._fetchCount('http://urls.api.twitter.com/1/urls/count.json?url={pageURL}', $.proxy(function(data) {
2968 if (data.count) {
2969 this._ui.twitter.children('span.badge').show().text(data.count);
2970 }
2971 }, this));
2972 },
2973
2974 /**
2975 * Fetches cumulative vote sum from Reddit.
2976 */
2977 _fetchReddit: function() {
2978 if (window.location.protocol.match(/^https/)) return;
2979
2980 this._fetchCount('http://www.reddit.com/api/info.json?url={pageURL}', $.proxy(function(data) {
2981 if (data.data.children.length) {
2982 this._ui.reddit.children('span.badge').show().text(data.data.children[0].data.score);
2983 }
2984 }, this), 'jsonp');
2985 }
2986 });
2987
2988 /**
2989 * Handles user mention suggestions in CKEditors.
2990 *
2991 * Important: Objects of this class have to be created before the CKEditor
2992 * is initialized!
2993 */
2994 WCF.Message.UserMention = Class.extend({
2995 /**
2996 * name of the class used to get the user suggestions
2997 * @var string
2998 */
2999 _className: 'wcf\\data\\user\\UserAction',
3000
3001 /**
3002 * dropdown object
3003 * @var jQuery
3004 */
3005 _dropdown: null,
3006
3007 /**
3008 * dropdown menu object
3009 * @var jQuery
3010 */
3011 _dropdownMenu: null,
3012
3013 /**
3014 * suggestion item index, -1 if none is selected
3015 * @var integer
3016 */
3017 _itemIndex: -1,
3018
3019 /**
3020 * line height
3021 * @var integer
3022 */
3023 _lineHeight: null,
3024
3025 /**
3026 * current beginning of the mentioning
3027 * @var string
3028 */
3029 _mentionStart: '',
3030
3031 /**
3032 * Initalizes user suggestions for the CKEditor with the given textarea id.
3033 *
3034 * @param string editorID
3035 */
3036 init: function(editorID) {
3037 // temporary disable suggestions for Internet Explorer
3038 //
3039 // this issue is caused by misplacing the range within the parent element,
3040 // while the typed chars are appended to the preceeding text node, without
3041 // getting focused for some reason
3042 if ($.browser.msie) {
3043 return;
3044 }
3045
3046 this._textarea = $('#' + editorID);
3047
3048 // get associated (ready) CKEditor object and add event listeners
3049 CKEDITOR.on('instanceReady', $.proxy(function(event) {
3050 if (event.editor.name === this._textarea.wcfIdentify()) {
3051 this._ckEditor = event.editor;
3052 this._ckEditor.container.on('keyup', $.proxy(this._keyup, this));
3053 this._ckEditor.container.on('keydown', $.proxy(this._keydown, this));
3054 this._ckEditor.on('key', $.proxy(this._key, this));
3055
3056 this._dropdown = $(this._ckEditor.editable().$);
3057 this._dropdownMenu = $('<ul class="dropdownMenu userSuggestionList" />').appendTo(this._textarea.parent());
3058 WCF.Dropdown.initDropdownFragment(this._dropdown, this._dropdownMenu);
3059 }
3060 }, this));
3061
3062 this._proxy = new WCF.Action.Proxy({
3063 success: $.proxy(this._success, this)
3064 });
3065 },
3066
3067 /**
3068 * Clears the suggestion list.
3069 */
3070 _clearList: function() {
3071 this._hideList();
3072
3073 this._dropdownMenu.empty();
3074 },
3075
3076 /**
3077 * Handles a click on a list item suggesting a username.
3078 *
3079 * @param object event
3080 */
3081 _click: function(event) {
3082 this._setUsername($(event.currentTarget).data('username'));
3083 },
3084
3085 /**
3086 * Creates an item in the suggestion list with the given data.
3087 *
3088 * @return object
3089 */
3090 _createListItem: function(listItemData) {
3091 var $listItem = $('<li />').data('username', listItemData.label).click($.proxy(this._click, this)).appendTo(this._dropdownMenu);
3092
3093 var $box16 = $('<div />').addClass('box16').appendTo($listItem);
3094 $box16.append($(listItemData.icon).addClass('framed'));
3095 $box16.append($('<div />').append($('<span />').text(listItemData.label)));
3096 },
3097
3098 /**
3099 * Returns the offsets used to set the position of the user suggestion
3100 * dropdown.
3101 *
3102 * @return object
3103 */
3104 _getDropdownMenuPosition: function() {
3105 var $range = this._ckEditor.getSelection().getRanges()[0];
3106 var $orgRange = $range.clone();
3107 var $startOffset = $range.startOffset;
3108
3109 // move caret after the '@' sign
3110 $range.setStart($range.startContainer, $startOffset - this._mentionStart.length);
3111 $range.collapse(true);
3112 $range.select();
3113
3114 // create span with random id and add it in front of the '@' sign
3115 var $element = document.createElement('span');
3116 $node = new CKEDITOR.dom.node($element);
3117 $range.insertNode($node);
3118
3119 // get offsets of span
3120 $jElement = $($element);
3121 if ($.browser.opera) {
3122 // in opera, the span's height is 0 if it has no content
3123 $jElement.text(' ');
3124 }
3125 var $offsets = $jElement.offset();
3126 if (this._lineHeight === null) {
3127 this._lineHeight = $jElement.height();
3128 }
3129
3130 // merge text nodes before and after the temporary span element
3131 // to avoid split text nodes which were one node before inserting
3132 // the span element since split nodes can cause problems working
3133 // with ranges and then remove the merged text node
3134 if (!$.browser.msie || $element.previousSibling && $element.nextSibling) {
3135 $element.previousSibling.nodeValue += $element.nextSibling.nodeValue;
3136 $($element.nextSibling).remove();
3137 }
3138
3139 // reset caret position to original position at the end and make
3140 // sure that the range is the same in all browsers
3141 $range.setStart($orgRange.startContainer, $startOffset);
3142 $range.setEnd($orgRange.startContainer, $startOffset);
3143 $range.select();
3144
3145 // remove span
3146 $jElement.remove();
3147
3148 return $offsets;
3149 },
3150
3151 /**
3152 * Returns the parameters for the AJAX request.
3153 *
3154 * @return object
3155 */
3156 _getParameters: function() {
3157 return {
3158 data: {
3159 includeUserGroups: false,
3160 searchString: this._mentionStart
3161 }
3162 };
3163 },
3164
3165 /**
3166 * Returns the relevant text in front of the caret in the current line.
3167 *
3168 * @return string
3169 */
3170 _getTextLineInFrontOfCaret: function() {
3171 var $range = this._ckEditor.getSelection().getRanges()[0];
3172
3173 // if text is marked, user suggestions are disabled
3174 if (!$range.collapsed) {
3175 return '';
3176 }
3177
3178 var $text = $range.startContainer.getText().substr(0, $range.startOffset);
3179
3180 // remove unicode zero width space and no-breaking space
3181 var $textBackup = $text;
3182 $text = '';
3183 for (var $i = 0; $i < $textBackup.length; $i++) {
3184 var $byte = $textBackup.charCodeAt($i).toString(16);
3185 if ($byte != '200b' && !/\s/.test($textBackup[$i])) {
3186 if ($textBackup[$i] === '@' && $i && /\s/.test($textBackup[$i - 1])) {
3187 $text = '';
3188 }
3189
3190 $text += $textBackup[$i];
3191 }
3192 else {
3193 $text = '';
3194 }
3195 }
3196
3197 return $text;
3198 },
3199
3200 /**
3201 * Hides the suggestion list.
3202 */
3203 _hideList: function() {
3204 this._dropdown.removeClass('dropdownOpen');
3205 this._dropdownMenu.removeClass('dropdownOpen');
3206
3207 this._itemIndex = -1;
3208 },
3209
3210 /**
3211 * Handles the key event of the CKEditor to select user suggestion on enter.
3212 */
3213 _key: function(event) {
3214 if (this._ckEditor.mode !== 'wysiwyg') {
3215 return true;
3216 }
3217
3218 if (this._dropdownMenu.is(':visible')) {
3219 if (event.data.keyCode === 13) { // enter
3220 this._dropdownMenu.children('li').eq(this._itemIndex).trigger('click');
3221
3222 event.cancel();
3223 }
3224 }
3225 },
3226
3227 /**
3228 * Handles the keydown event to check if the user starts mentioning someone.
3229 *
3230 * @param object event
3231 */
3232 _keydown: function(event) {
3233 if (this._ckEditor.mode !== 'wysiwyg') {
3234 return true;
3235 }
3236
3237 if (this._dropdownMenu.is(':visible')) {
3238 switch (event.data.$.keyCode) {
3239 case 38: // arrow up
3240 event.data.$.preventDefault();
3241
3242 this._selectItem(this._itemIndex - 1);
3243 break;
3244
3245 case 40: // arrow down
3246 event.data.$.preventDefault();
3247
3248 this._selectItem(this._itemIndex + 1);
3249 break;
3250 }
3251 }
3252 },
3253
3254 /**
3255 * Handles the keyup event to check if the user starts mentioning someone.
3256 *
3257 * @param object event
3258 */
3259 _keyup: function(event) {
3260 if (this._ckEditor.mode !== 'wysiwyg') {
3261 return true;
3262 }
3263
3264 // ignore enter key up event
3265 if (event.data.$.keyCode === 13) {
3266 return;
3267 }
3268
3269 // ignore event if suggestion list and user pressed enter, arrow up or arrow down
3270 if (this._dropdownMenu.is(':visible') && event.data.$.keyCode in { 13:1, 38:1, 40:1 }) {
3271 return;
3272 }
3273
3274 var $currentText = this._getTextLineInFrontOfCaret();
3275 if ($currentText) {
3276 var $match = $currentText.match(/@([^,]{3,})$/);
3277 if ($match) {
3278 // if mentioning is at text begin or there's a whitespace character
3279 // before the '@', everything is fine
3280 if (!$match.index || $currentText[$match.index - 1].match(/\s/)) {
3281 this._mentionStart = $match[1];
3282
3283 this._proxy.setOption('data', {
3284 actionName: 'getSearchResultList',
3285 className: this._className,
3286 interfaceName: 'wcf\\data\\ISearchAction',
3287 parameters: this._getParameters()
3288 });
3289 this._proxy.sendRequest();
3290 }
3291 }
3292 else {
3293 this._hideList();
3294 }
3295 }
3296 else {
3297 this._hideList();
3298 }
3299 },
3300
3301 /**
3302 * Replaces the started mentioning with a chosen username.
3303 */
3304 _setUsername: function(username) {
3305 var $range = this._ckEditor.getSelection().getRanges()[0];
3306
3307 // remove the beginning of the username and the '@'
3308 $range.setStart($range.startContainer, $range.startOffset - (this._mentionStart.length + 1));
3309
3310 // if an existing mention is edited, remove the link around it
3311 var $removedLink = false;
3312 var $commonAncestor = $range.getCommonAncestor();
3313 if ($commonAncestor.getText() == '@' + this._mentionStart) {
3314 if ($commonAncestor.getParent() && $commonAncestor.getParent().getName() == 'a') {
3315 $commonAncestor.replace($commonAncestor.getParent());
3316 $range.setStart($commonAncestor);
3317 }
3318 }
3319 $range.deleteContents();
3320
3321 // insert username
3322 if (username.indexOf("'") !== -1) {
3323 username = username.replace(/'/g, "''");
3324 username = "'" + username + "'";
3325 }
3326 else if (username.indexOf(' ') !== -1) {
3327 username = "'" + username + "'";
3328 }
3329 var $usernameNode = new CKEDITOR.dom.text('@' + username);
3330 $range.insertNode($usernameNode);
3331 $range.selectNodeContents($usernameNode);
3332 $range.collapse(false);
3333 $range.select();
3334
3335 this._hideList();
3336 },
3337
3338 /**
3339 * Selects the suggestion with the given item index.
3340 *
3341 * @param integer itemIndex
3342 */
3343 _selectItem: function(itemIndex) {
3344 var $li = this._dropdownMenu.children('li');
3345
3346 if (itemIndex < 0) {
3347 itemIndex = $li.length - 1;
3348 }
3349 else if (itemIndex + 1 > $li.length) {
3350 itemIndex = 0;
3351 }
3352
3353 $li.removeClass('dropdownNavigationItem');
3354 $li.eq(itemIndex).addClass('dropdownNavigationItem');
3355
3356 this._itemIndex = itemIndex;
3357 },
3358
3359 /**
3360 * Shows the suggestion list.
3361 */
3362 _showList: function() {
3363 this._dropdown.addClass('dropdownOpen');
3364 this._dropdownMenu.addClass('dropdownOpen');
3365 },
3366
3367 /**
3368 * Evalutes user suggestion-AJAX request results.
3369 *
3370 * @param object data
3371 * @param string textStatus
3372 * @param jQuery jqXHR
3373 */
3374 _success: function(data, textStatus, jqXHR) {
3375 this._clearList(false);
3376
3377 if ($.getLength(data.returnValues)) {
3378 for (var $i in data.returnValues) {
3379 var $item = data.returnValues[$i];
3380 this._createListItem($item);
3381 }
3382
3383 this._updateSuggestionListPosition();
3384 this._showList();
3385 }
3386 },
3387
3388 /**
3389 * Updates the position of the suggestion list.
3390 */
3391 _updateSuggestionListPosition: function() {
3392 try {
3393 var $dropdownMenuPosition = this._getDropdownMenuPosition();
3394 $dropdownMenuPosition.top += 5 + this._lineHeight; // add little vertical gap
3395 $dropdownMenuPosition.left -= 16; // make sure dropdown arrow is at correct position
3396 this._dropdownMenu.css($dropdownMenuPosition);
3397 this._selectItem(0);
3398
3399 if ($dropdownMenuPosition.top + this._dropdownMenu.outerHeight() + 10 > $(window).height() + $(document).scrollTop()) {
3400 this._dropdownMenu.addClass('dropdownArrowBottom');
3401
3402 this._dropdownMenu.css({
3403 top: $dropdownMenuPosition.top - this._dropdownMenu.outerHeight() - 2 * this._lineHeight + 5
3404 });
3405 }
3406 else {
3407 this._dropdownMenu.removeClass('dropdownArrowBottom');
3408 }
3409 }
3410 catch (e) {
3411 // ignore errors that are caused by pressing enter to
3412 // often in a short period of time
3413 }
3414 }
3415 });