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