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