Merged com.woltlab.wcf.message into WCF
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WCF.Message.js
1 /**
2 * Message related classes for WCF
3 *
4 * @author Alexander Ebert
5 * @copyright 2001-2013 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 */
8 WCF.Message = { };
9
10 /**
11 * Namespace for BBCode related classes.
12 */
13 WCF.Message.BBCode = { };
14
15 /**
16 * BBCode Viewer for WCF.
17 */
18 WCF.Message.BBCode.CodeViewer = Class.extend({
19 /**
20 * dialog overlay
21 * @var jQuery
22 */
23 _dialog: null,
24
25 /**
26 * Initializes the WCF.Message.BBCode.CodeViewer class.
27 */
28 init: function() {
29 this._dialog = null;
30
31 this._initCodeBoxes();
32
33 WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.BBCode.CodeViewer', $.proxy(this._initCodeBoxes, this));
34 WCF.DOMNodeInsertedHandler.forceExecution();
35 },
36
37 /**
38 * Initializes available code boxes.
39 */
40 _initCodeBoxes: function() {
41 $('.codeBox:not(.jsCodeViewer)').each($.proxy(function(index, codeBox) {
42 var $codeBox = $(codeBox).addClass('jsCodeViewer');
43
44 $('<span class="icon icon16 icon-copy pointer jsTooltip" title="' + WCF.Language.get('wcf.message.bbcode.code.copy') + '" />').appendTo($codeBox.find('div > h3')).click($.proxy(this._click, this));
45 }, this));
46 },
47
48 /**
49 * Shows a code viewer for a specific code box.
50 *
51 * @param object event
52 */
53 _click: function(event) {
54 var $content = '';
55 $(event.currentTarget).parents('div').next('ol').children('li').each(function(index, listItem) {
56 if ($content) {
57 $content += "\n";
58 }
59
60 // do *not* use $.trim here, as we want to preserve whitespaces whitespaces
61 $content += $(listItem).text().replace(/\n+$/, '');
62 });
63
64
65 if (this._dialog === null) {
66 this._dialog = $('<div><textarea cols="60" rows="12" readonly="readonly" /></div>').hide().appendTo(document.body);
67 this._dialog.children('textarea').val($content);
68 this._dialog.wcfDialog({
69 title: WCF.Language.get('wcf.message.bbcode.code.copy')
70 });
71 }
72 else {
73 this._dialog.children('textarea').val($content);
74 this._dialog.wcfDialog('open');
75 }
76
77 this._dialog.children('textarea').select();
78 }
79 });
80
81 /**
82 * Prevents multiple submits of the same form by disabling the submit button.
83 */
84 WCF.Message.FormGuard = Class.extend({
85 /**
86 * Initializes the WCF.Message.FormGuard class.
87 */
88 init: function() {
89 var $forms = $('form.jsFormGuard').removeClass('jsFormGuard').submit(function() {
90 $(this).find('.formSubmit input[type=submit]').disable();
91 });
92
93 // restore buttons, prevents disabled buttons on back navigation in Opera
94 $(window).unload(function() {
95 $forms.find('.formSubmit input[type=submit]').enable();
96 });
97 }
98 });
99
100 /**
101 * Provides previews for ckEditor message fields.
102 *
103 * @param string className
104 * @param string messageFieldID
105 * @param string previewButtonID
106 */
107 WCF.Message.Preview = Class.extend({
108 /**
109 * class name
110 * @var string
111 */
112 _className: '',
113
114 /**
115 * message field id
116 * @var string
117 */
118 _messageFieldID: '',
119
120 /**
121 * message field
122 * @var jQuery
123 */
124 _messageField: null,
125
126 /**
127 * action proxy
128 * @var WCF.Action.Proxy
129 */
130 _proxy: null,
131
132 /**
133 * preview button
134 * @var jQuery
135 */
136 _previewButton: null,
137
138 /**
139 * previous button label
140 * @var string
141 */
142 _previewButtonLabel: '',
143
144 /**
145 * Initializes a new WCF.Message.Preview object.
146 *
147 * @param string className
148 * @param string messageFieldID
149 * @param string previewButtonID
150 */
151 init: function(className, messageFieldID, previewButtonID) {
152 this._className = className;
153
154 // validate message field
155 this._messageFieldID = $.wcfEscapeID(messageFieldID);
156 this._messageField = $('#' + this._messageFieldID);
157 if (!this._messageField.length) {
158 console.debug("[WCF.Message.Preview] Unable to find message field identified by '" + this._messageFieldID + "'");
159 return;
160 }
161
162 // validate preview button
163 previewButtonID = $.wcfEscapeID(previewButtonID);
164 this._previewButton = $('#' + previewButtonID);
165 if (!this._previewButton.length) {
166 console.debug("[WCF.Message.Preview] Unable to find preview button identified by '" + previewButtonID + "'");
167 return;
168 }
169
170 this._previewButton.click($.proxy(this._click, this));
171 this._proxy = new WCF.Action.Proxy({
172 success: $.proxy(this._success, this)
173 });
174 },
175
176 /**
177 * Reads message field input and triggers an AJAX request.
178 */
179 _click: function(event) {
180 var $message = this._getMessage();
181 if ($message === null) {
182 console.debug("[WCF.Message.Preview] Unable to access ckEditor instance of '" + this._messageFieldID + "'");
183 return;
184 }
185
186 this._proxy.setOption('data', {
187 actionName: 'getMessagePreview',
188 className: this._className,
189 parameters: this._getParameters($message)
190 });
191 this._proxy.sendRequest();
192
193 // update button label
194 this._previewButtonLabel = this._previewButton.html();
195 this._previewButton.html(WCF.Language.get('wcf.global.loading')).disable();
196
197 // poke event
198 event.stopPropagation();
199 return false;
200 },
201
202 /**
203 * Returns request parameters.
204 *
205 * @param string message
206 * @return object
207 */
208 _getParameters: function(message) {
209 // collect message form options
210 var $options = { };
211 $('#settings').find('input[type=checkbox]').each(function(index, checkbox) {
212 var $checkbox = $(checkbox);
213 if ($checkbox.is(':checked')) {
214 $options[$checkbox.prop('name')] = $checkbox.prop('value');
215 }
216 });
217
218 // build parameters
219 return {
220 data: {
221 message: message
222 },
223 options: $options
224 };
225 },
226
227 /**
228 * Returns parsed message from ckEditor or null if editor was not accessible.
229 *
230 * @return string
231 */
232 _getMessage: function() {
233 if ($.browser.mobile) {
234 return this._messageField.val();
235 }
236 else if (this._messageField.data('ckeditorInstance')) {
237 var $ckEditor = this._messageField.ckeditorGet();
238 return $ckEditor.getData();
239 }
240
241 return null;
242 },
243
244 /**
245 * Handles successful AJAX requests.
246 *
247 * @param object data
248 * @param string textStatus
249 * @param jQuery jqXHR
250 */
251 _success: function(data, textStatus, jqXHR) {
252 // restore preview button
253 this._previewButton.html(this._previewButtonLabel).enable();
254
255 // evaluate message
256 this._handleResponse(data);
257 },
258
259 /**
260 * Evaluates response data.
261 *
262 * @param object data
263 */
264 _handleResponse: function(data) { }
265 });
266
267 /**
268 * Default implementation for message previews.
269 *
270 * @see WCF.Message.Preview
271 */
272 WCF.Message.DefaultPreview = WCF.Message.Preview.extend({
273 _attachmentObjectType: null,
274 _attachmentObjectID: null,
275 _tmpHash: null,
276
277 /**
278 * @see WCF.Message.Preview.init()
279 */
280 init: function(attachmentObjectType, attachmentObjectID, tmpHash) {
281 this._super('wcf\\data\\bbcode\\MessagePreviewAction', 'text', 'previewButton');
282
283 this._attachmentObjectType = attachmentObjectType || null;
284 this._attachmentObjectID = attachmentObjectID || null;
285 this._tmpHash = tmpHash || null;
286 },
287
288 /**
289 * @see WCF.Message.Preview._handleResponse()
290 */
291 _handleResponse: function(data) {
292 var $preview = $('#previewContainer');
293 if (!$preview.length) {
294 $preview = $('<div class="container containerPadding marginTop" id="previewContainer"><fieldset><legend>' + WCF.Language.get('wcf.global.preview') + '</legend><div></div></fieldset>').prependTo($('#messageContainer')).wcfFadeIn();
295 }
296
297 $preview.find('div:eq(0)').html(data.returnValues.message);
298 },
299
300 /**
301 * @see WCF.Message.Preview._getParameters()
302 */
303 _getParameters: function(message) {
304 var $parameters = this._super(message);
305
306 if (this._attachmentObjectType != null) {
307 $parameters.attachmentObjectType = this._attachmentObjectType;
308 $parameters.attachmentObjectID = this._attachmentObjectID;
309 $parameters.tmpHash = this._tmpHash;
310 }
311
312 return $parameters;
313 }
314 });
315
316 /**
317 * Handles multilingualism for messages.
318 *
319 * @param integer languageID
320 * @param object availableLanguages
321 * @param boolean forceSelection
322 */
323 WCF.Message.Multilingualism = Class.extend({
324 /**
325 * list of available languages
326 * @var object
327 */
328 _availableLanguages: { },
329
330 /**
331 * language id
332 * @var integer
333 */
334 _languageID: 0,
335
336 /**
337 * language input element
338 * @var jQuery
339 */
340 _languageInput: null,
341
342 /**
343 * Initializes WCF.Message.Multilingualism
344 *
345 * @param integer languageID
346 * @param object availableLanguages
347 * @param boolean forceSelection
348 */
349 init: function(languageID, availableLanguages, forceSelection) {
350 this._availableLanguages = availableLanguages;
351 this._languageID = languageID || 0;
352
353 this._languageInput = $('#languageID');
354
355 // preselect current language id
356 this._updateLabel();
357
358 // register event listener
359 this._languageInput.find('.dropdownMenu > li').click($.proxy(this._click, this));
360
361 // add element to disable multilingualism
362 if (!forceSelection) {
363 var $dropdownMenu = this._languageInput.find('.dropdownMenu');
364 $('<li class="dropdownDivider" />').appendTo($dropdownMenu);
365 $('<li><span><span class="badge">' + this._availableLanguages[0] + '</span></span></li>').click($.proxy(this._disable, this)).appendTo($dropdownMenu);
366 }
367
368 // bind submit event
369 this._languageInput.parents('form').submit($.proxy(this._submit, this));
370 },
371
372 /**
373 * Handles language selections.
374 *
375 * @param object event
376 */
377 _click: function(event) {
378 this._languageID = $(event.currentTarget).data('languageID');
379 this._updateLabel();
380 },
381
382 /**
383 * Disables language selection.
384 */
385 _disable: function() {
386 this._languageID = 0;
387 this._updateLabel();
388 },
389
390 /**
391 * Updates selected language.
392 */
393 _updateLabel: function() {
394 this._languageInput.find('.dropdownToggle > span').text(this._availableLanguages[this._languageID]);
395 },
396
397 /**
398 * Sets language id upon submit.
399 */
400 _submit: function() {
401 this._languageInput.next('input[name=languageID]').prop('value', this._languageID);
402 }
403 });
404
405 /**
406 * Loads smiley categories upon user request.
407 */
408 WCF.Message.SmileyCategories = Class.extend({
409 /**
410 * list of already loaded category ids
411 * @var array<integer>
412 */
413 _cache: [ ],
414
415 /**
416 * action proxy
417 * @var WCF.Action.Proxy
418 */
419 _proxy: null,
420
421 /**
422 * ckEditor element
423 * @var jQuery
424 */
425 _ckEditor: null,
426
427 /**
428 * Initializes the smiley loader.
429 *
430 * @param string ckEditorID
431 */
432 init: function() {
433 this._cache = [ ];
434 this._proxy = new WCF.Action.Proxy({
435 success: $.proxy(this._success, this)
436 });
437
438 $('#smilies').on('wcftabsbeforeactivate', $.proxy(this._click, this));
439
440 // handle onload
441 var self = this;
442 new WCF.PeriodicalExecuter(function(pe) {
443 pe.stop();
444
445 self._click({ }, { newTab: $('#smilies > .menu li.ui-state-active') });
446 }, 100);
447 },
448
449 /**
450 * Handles tab menu clicks.
451 *
452 * @param object event
453 * @param object ui
454 */
455 _click: function(event, ui) {
456 var $categoryID = parseInt($(ui.newTab).children('a').data('smileyCategoryID'));
457
458 if ($categoryID && !WCF.inArray($categoryID, this._cache)) {
459 this._proxy.setOption('data', {
460 actionName: 'getSmilies',
461 className: 'wcf\\data\\smiley\\category\\SmileyCategoryAction',
462 objectIDs: [ $categoryID ]
463 });
464 this._proxy.sendRequest();
465 }
466 },
467
468 /**
469 * Handles successful AJAX requests.
470 *
471 * @param object data
472 * @param string textStatus
473 * @param jQuery jqXHR
474 */
475 _success: function(data, textStatus, jqXHR) {
476 var $categoryID = parseInt(data.returnValues.smileyCategoryID);
477 this._cache.push($categoryID);
478
479 $('#smilies-' + $categoryID).html(data.returnValues.template);
480 }
481 });
482
483 /**
484 * Handles smiley clicks.
485 */
486 WCF.Message.Smilies = Class.extend({
487 /**
488 * ckEditor element
489 * @var jQuery
490 */
491 _ckEditor: null,
492
493 /**
494 * Initializes the smiley handler.
495 *
496 * @param string ckEditorID
497 */
498 init: function(ckEditorID) {
499 // get ck editor
500 if (ckEditorID) {
501 this._ckEditor = $('#' + ckEditorID);
502
503 // add smiley click handler
504 $(document).on('click', '.jsSmiley', $.proxy(this._smileyClick, this));
505 }
506 },
507
508 /**
509 * Handles tab smiley clicks.
510 *
511 * @param object event
512 */
513 _smileyClick: function(event) {
514 var $target = $(event.currentTarget);
515 var $smileyCode = $target.data('smileyCode');
516
517 // get ckEditor
518 var $ckEditor = this._ckEditor.ckeditorGet();
519 // get smiley path
520 var $smileyPath = $target.find('img').attr('src');
521
522 // add smiley to config
523 if (!WCF.inArray($smileyCode, $ckEditor.config.smiley_descriptions)) {
524 $ckEditor.config.smiley_descriptions.push($smileyCode);
525 $ckEditor.config.smiley_images.push($smileyPath);
526 }
527
528 if ($ckEditor.mode === 'wysiwyg') {
529 // in design mode
530 var $img = $ckEditor.document.createElement('img', {
531 attributes: {
532 src: $smileyPath,
533 'class': 'smiley',
534 alt: $smileyCode
535 }
536 });
537 $ckEditor.insertText(' ');
538 $ckEditor.insertElement($img);
539 $ckEditor.insertText(' ');
540 }
541 else {
542 // in source mode
543 var $textarea = this._ckEditor.next('.cke_editor_text').find('textarea');
544 var $value = $textarea.val();
545 if ($value.length == 0) {
546 $textarea.val($smileyCode);
547 $textarea.setCaret($smileyCode.length);
548 }
549 else {
550 var $position = $textarea.getCaret();
551 var $string = (($value.substr($position - 1, 1) !== ' ') ? ' ' : '') + $smileyCode + ' ';
552 $textarea.val( $value.substr(0, $position) + $string + $value.substr($position) );
553 $textarea.setCaret($position + $string.length);
554 }
555 }
556 }
557 });
558
559 /**
560 * Provides an AJAX-based quick reply for messages.
561 */
562 WCF.Message.QuickReply = Class.extend({
563 /**
564 * quick reply container
565 * @var jQuery
566 */
567 _container: null,
568
569 /**
570 * message field
571 * @var jQuery
572 */
573 _messageField: null,
574
575 /**
576 * notification object
577 * @var WCF.System.Notification
578 */
579 _notification: null,
580
581 /**
582 * action proxy
583 * @var WCF.Action.Proxy
584 */
585 _proxy: null,
586
587 /**
588 * quote manager object
589 * @var WCF.Message.Quote.Manager
590 */
591 _quoteManager: null,
592
593 /**
594 * scroll handler
595 * @var WCF.Effect.Scroll
596 */
597 _scrollHandler: null,
598
599 /**
600 * success message for created but invisible messages
601 * @var string
602 */
603 _successMessageNonVisible: '',
604
605 /**
606 * Initializes a new WCF.Message.QuickReply object.
607 *
608 * @param boolean supportExtendedForm
609 * @param WCF.Message.Quote.Manager quoteManager
610 */
611 init: function(supportExtendedForm, quoteManager) {
612 this._container = $('#messageQuickReply');
613 this._messageField = $('#text');
614 if (!this._container || !this._messageField) {
615 return;
616 }
617
618 // button actions
619 var $formSubmit = this._container.find('.formSubmit');
620 $formSubmit.find('button[data-type=save]').click($.proxy(this._save, this));
621 if (supportExtendedForm) $formSubmit.find('button[data-type=extended]').click($.proxy(this._prepareExtended, this));
622 $formSubmit.find('button[data-type=cancel]').click($.proxy(this._cancel, this));
623
624 if (quoteManager) this._quoteManager = quoteManager;
625
626 $('.jsQuickReply').data('__api', this).click($.proxy(this.click, this));
627
628 this._proxy = new WCF.Action.Proxy({
629 failure: $.proxy(this._failure, this),
630 showLoadingOverlay: false,
631 success: $.proxy(this._success, this)
632 });
633 this._scroll = new WCF.Effect.Scroll();
634 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.add'));
635 this._successMessageNonVisible = '';
636 },
637
638 /**
639 * Handles clicks on reply button.
640 *
641 * @param object event
642 */
643 click: function(event) {
644 this._container.toggle();
645
646 if (this._container.is(':visible')) {
647 this._scroll.scrollTo(this._container, true);
648
649 WCF.Message.Submit.registerButton('text', this._container.find('.formSubmit button[data-type=save]'));
650
651 if (this._quoteManager) {
652 // check if message field is empty
653 var $empty = true;
654 if ($.browser.touch) {
655 $empty = (!this._messageField.val().length);
656 }
657 else {
658 $empty = (!this._messageField.ckeditorGet().getData().length);
659 }
660
661 if ($empty) {
662 this._quoteManager.insertQuotes(this._getClassName(), this._getObjectID(), $.proxy(this._insertQuotes, this));
663 }
664 }
665
666 new WCF.PeriodicalExecuter($.proxy(function(pe) {
667 pe.stop();
668
669 if ($.browser.mobile) {
670 this._messageField.focus();
671 }
672 else {
673 this._messageField.ckeditorGet().ui.editor.focus();
674 }
675 }, this), 250);
676 }
677
678 // discard event
679 if (event !== null) {
680 event.stopPropagation();
681 return false;
682 }
683 },
684
685 /**
686 * Returns container element.
687 *
688 * @return jQuery
689 */
690 getContainer: function() {
691 return this._container;
692 },
693
694 /**
695 * Insertes quotes into the quick reply editor.
696 *
697 * @param object data
698 */
699 _insertQuotes: function(data) {
700 if (!data.returnValues.template) {
701 return;
702 }
703
704 if ($.browser.mobile) {
705 this._messageField.val(data.returnValues.template);
706 }
707 else {
708 this._messageField.ckeditorGet().insertText(data.returnValues.template);
709 }
710 },
711
712 /**
713 * Saves message.
714 */
715 _save: function() {
716 var $message = '';
717
718 if ($.browser.mobile) {
719 $message = $.trim(this._messageField.val());
720 }
721 else {
722 var $ckEditor = this._messageField.ckeditorGet();
723 $message = $.trim($ckEditor.getData());
724 }
725
726 // check if message is empty
727 var $innerError = this._messageField.parent().find('small.innerError');
728 if ($message === '') {
729 if (!$innerError.length) {
730 $innerError = $('<small class="innerError" />').appendTo(this._messageField.parent());
731 }
732
733 $innerError.html(WCF.Language.get('wcf.global.form.error.empty'));
734 return;
735 }
736 else {
737 $innerError.remove();
738 }
739
740 this._proxy.setOption('data', {
741 actionName: 'quickReply',
742 className: this._getClassName(),
743 interfaceName: 'wcf\\data\\IMessageQuickReplyAction',
744 parameters: this._getParameters($message)
745 });
746 this._proxy.sendRequest();
747
748 // show spinner and hide CKEditor
749 var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
750 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody);
751 $messageBody.children('#cke_text').hide().end().next().hide();
752 },
753
754 /**
755 * Returns the parameters for the save request.
756 *
757 * @param string message
758 * @return object
759 */
760 _getParameters: function(message) {
761 var $parameters = {
762 objectID: this._getObjectID(),
763 data: {
764 message: message
765 },
766 lastPostTime: this._container.data('lastPostTime'),
767 pageNo: this._container.data('pageNo'),
768 removeQuoteIDs: (this._quoteManager === null ? [ ] : this._quoteManager.getQuotesMarkedForRemoval())
769 };
770 if (this._container.data('anchor')) {
771 $parameters.anchor = this._container.data('anchor');
772 }
773
774 return $parameters;
775 },
776
777 /**
778 * Cancels quick reply.
779 */
780 _cancel: function() {
781 this._revertQuickReply(true);
782
783 if ($.browser.mobile) {
784 this._messageField.val('');
785 }
786 else {
787 // revert CKEditor
788 this._messageField.ckeditorGet().setData('');
789 }
790 },
791
792 /**
793 * Reverts quick reply to original state and optionally hiding it.
794 *
795 * @param boolean hide
796 */
797 _revertQuickReply: function(hide) {
798 var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
799
800 if (hide) {
801 this._container.hide();
802
803 // remove previous error messages
804 $messageBody.children('small.innerError').remove();
805 }
806
807 // display CKEditor
808 $messageBody.children('.icon-spinner').remove();
809 $messageBody.children('#cke_text').show();
810
811 // display form submit
812 $messageBody.next().show();
813 },
814
815 /**
816 * Prepares jump to extended message add form.
817 */
818 _prepareExtended: function() {
819 // mark quotes for removal
820 if (this._quoteManager !== null) {
821 this._quoteManager.markQuotesForRemoval();
822 }
823
824 var $message = '';
825
826 if ($.browser.mobile) {
827 $message = this._messageField.val();
828 }
829 else {
830 var $ckEditor = this._messageField.ckeditorGet();
831 $message = $ckEditor.getData();
832 }
833
834 new WCF.Action.Proxy({
835 autoSend: true,
836 data: {
837 actionName: 'jumpToExtended',
838 className: this._getClassName(),
839 interfaceName: 'wcf\\data\\IExtendedMessageQuickReplyAction',
840 parameters: {
841 containerID: this._getObjectID(),
842 message: $message
843 }
844 },
845 success: function(data, textStatus, jqXHR) {
846 window.location = data.returnValues.url;
847 }
848 });
849 },
850
851 /**
852 * Handles successful AJAX calls.
853 *
854 * @param object data
855 * @param string textStatus
856 * @param jQuery jqXHR
857 */
858 _success: function(data, textStatus, jqXHR) {
859 // redirect to new page
860 if (data.returnValues.url) {
861 window.location = data.returnValues.url;
862 }
863 else {
864 if (data.returnValues.template) {
865 // insert HTML
866 var $message = $('' + data.returnValues.template);
867 $message.insertBefore(this._container);
868
869 // update last post time
870 this._container.data('lastPostTime', data.returnValues.lastPostTime);
871
872 // show notification
873 this._notification.show(undefined, undefined, WCF.Language.get('wcf.global.success.add'));
874
875 this._updateHistory($message.wcfIdentify());
876 }
877 else {
878 // show notification
879 var $message = (this._successMessageNonVisible) ? this._successMessageNonVisible : 'wcf.global.success.add';
880 this._notification.show(undefined, 5000, WCF.Language.get($message));
881 }
882
883 if ($.browser.mobile) {
884 this._messageField.val('');
885 }
886 else {
887 // remove CKEditor contents
888 this._messageField.ckeditorGet().setData('');
889 }
890
891 // hide quick reply and revert it
892 this._revertQuickReply(true);
893
894 // count stored quotes
895 if (this._quoteManager !== null) {
896 this._quoteManager.countQuotes();
897 }
898 }
899 },
900
901 /**
902 * Reverts quick reply on failure to preserve entered message.
903 */
904 _failure: function(data) {
905 this._revertQuickReply(false);
906
907 if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
908 return true;
909 }
910
911 var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
912 var $innerError = $messageBody.children('small.innerError').empty();
913 if (!$innerError.length) {
914 $innerError = $('<small class="innerError" />').appendTo($messageBody);
915 }
916
917 $innerError.html(data.returnValues.errorType);
918
919 return false;
920 },
921
922 /**
923 * Returns action class name.
924 *
925 * @return string
926 */
927 _getClassName: function() {
928 return '';
929 },
930
931 /**
932 * Returns object id.
933 *
934 * @return integer
935 */
936 _getObjectID: function() {
937 return 0;
938 },
939
940 /**
941 * Updates the history to avoid old content when going back in the browser
942 * history.
943 *
944 * @param hash
945 */
946 _updateHistory: function(hash) {
947 window.location.hash = hash;
948 }
949 });
950
951 /**
952 * Provides an inline message editor.
953 *
954 * @param integer containerID
955 */
956 WCF.Message.InlineEditor = Class.extend({
957 /**
958 * currently active message
959 * @var string
960 */
961 _activeElementID: '',
962
963 /**
964 * message cache
965 * @var string
966 */
967 _cache: '',
968
969 /**
970 * list of messages
971 * @var object
972 */
973 _container: { },
974
975 /**
976 * container id
977 * @var integer
978 */
979 _containerID: 0,
980
981 /**
982 * list of dropdowns
983 * @var object
984 */
985 _dropdowns: { },
986
987 /**
988 * CSS selector for the message container
989 * @var string
990 */
991 _messageContainerSelector: '.jsMessage',
992
993 /**
994 * prefix of the message editor CSS id
995 * @var string
996 */
997 _messageEditorIDPrefix: 'messageEditor',
998
999 /**
1000 * notification object
1001 * @var WCF.System.Notification
1002 */
1003 _notification: null,
1004
1005 /**
1006 * proxy object
1007 * @var WCF.Action.Proxy
1008 */
1009 _proxy: null,
1010
1011 /**
1012 * support for extended editing form
1013 * @var boolean
1014 */
1015 _supportExtendedForm: false,
1016
1017 /**
1018 * Initializes a new WCF.Message.InlineEditor object.
1019 *
1020 * @param integer containerID
1021 * @param boolean supportExtendedForm
1022 */
1023 init: function(containerID, supportExtendedForm) {
1024 this._activeElementID = '';
1025 this._cache = '';
1026 this._container = { };
1027 this._containerID = parseInt(containerID);
1028 this._dropdowns = { };
1029 this._supportExtendedForm = (supportExtendedForm) ? true : false;
1030 this._proxy = new WCF.Action.Proxy({
1031 failure: $.proxy(this._failure, this),
1032 showLoadingOverlay: false,
1033 success: $.proxy(this._success, this)
1034 });
1035 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.edit'));
1036
1037 this.initContainers();
1038
1039 WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.InlineEditor', $.proxy(this.initContainers, this));
1040 },
1041
1042 /**
1043 * Initializes editing capability for all messages.
1044 */
1045 initContainers: function() {
1046 $(this._messageContainerSelector).each($.proxy(function(index, container) {
1047 var $container = $(container);
1048 var $containerID = $container.wcfIdentify();
1049
1050 if (!this._container[$containerID]) {
1051 this._container[$containerID] = $container;
1052
1053 if ($container.data('canEditInline')) {
1054 $container.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID).click($.proxy(this._clickInline, this)).dblclick($.proxy(this._click, this));
1055 }
1056 else if ($container.data('canEdit')) {
1057 $container.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID).click($.proxy(this._click, this));
1058 }
1059 }
1060 }, this));
1061 },
1062
1063 /**
1064 * Loads WYSIWYG editor for selected message.
1065 *
1066 * @param object event
1067 * @param integer containerID
1068 * @return boolean
1069 */
1070 _click: function(event, containerID) {
1071 var $containerID = (event === null) ? containerID : $(event.currentTarget).data('containerID');
1072
1073 if (this._activeElementID === '') {
1074 this._activeElementID = $containerID;
1075 this._prepare();
1076
1077 this._proxy.setOption('data', {
1078 actionName: 'beginEdit',
1079 className: this._getClassName(),
1080 interfaceName: 'wcf\\data\\IMessageInlineEditorAction',
1081 parameters: {
1082 containerID: this._containerID,
1083 objectID: this._container[$containerID].data('objectID')
1084 }
1085 });
1086 this._proxy.sendRequest();
1087 }
1088 else {
1089 var $notification = new WCF.System.Notification(WCF.Language.get('wcf.message.error.editorAlreadyInUse'), 'warning');
1090 $notification.show();
1091 }
1092
1093 if (event !== null) {
1094 event.stopPropagation();
1095 return false;
1096 }
1097 },
1098
1099 /**
1100 * Provides an inline dropdown menu instead of directly loading the WYSIWYG editor.
1101 *
1102 * @param object event
1103 * @return boolean
1104 */
1105 _clickInline: function(event) {
1106 var $button = $(event.currentTarget);
1107
1108 if (!$button.hasClass('dropdownToggle')) {
1109 var $containerID = $button.data('containerID');
1110
1111 WCF.DOMNodeInsertedHandler.enable();
1112
1113 $button.addClass('dropdownToggle').parent().addClass('dropdown');
1114
1115 var $dropdownMenu = $('<ul class="dropdownMenu" />').insertAfter($button);
1116 this._initDropdownMenu($containerID, $dropdownMenu);
1117
1118 WCF.DOMNodeInsertedHandler.disable();
1119
1120 this._dropdowns[this._container[$containerID].data('objectID')] = $dropdownMenu;
1121
1122 WCF.Dropdown.registerCallback($button.parent().wcfIdentify(), $.proxy(this._toggleDropdown, this));
1123
1124 // trigger click event
1125 $button.trigger('click');
1126 }
1127
1128 event.stopPropagation();
1129 return false;
1130 },
1131
1132 /**
1133 * Handles errorneus editing requests.
1134 *
1135 * @param object data
1136 */
1137 _failure: function(data) {
1138 this._revertEditor();
1139
1140 if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
1141 return true;
1142 }
1143
1144 var $messageBody = this._container[this._activeElementID].find('.messageBody .messageInlineEditor');
1145 var $innerError = $messageBody.children('small.innerError').empty();
1146 if (!$innerError.length) {
1147 $innerError = $('<small class="innerError" />').insertBefore($messageBody.children('.formSubmit'));
1148 }
1149
1150 $innerError.html(data.returnValues.errorType);
1151
1152 return false;
1153 },
1154
1155 /**
1156 * Forces message options to stay visible if toggling dropdown menu.
1157 *
1158 * @param jQuery dropdown
1159 * @param string action
1160 */
1161 _toggleDropdown: function(dropdown, action) {
1162 dropdown.parents('.messageOptions').toggleClass('forceOpen');
1163 },
1164
1165 /**
1166 * Initializes the inline edit dropdown menu.
1167 *
1168 * @param integer containerID
1169 * @param jQuery dropdownMenu
1170 */
1171 _initDropdownMenu: function(containerID, dropdownMenu) { },
1172
1173 /**
1174 * Prepares message for WYSIWYG display.
1175 */
1176 _prepare: function() {
1177 var $messageBody = this._container[this._activeElementID].find('.messageBody');
1178 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody);
1179
1180 var $content = $messageBody.find('.messageText');
1181 this._cache = $content.html();
1182 $content.empty();
1183 },
1184
1185 /**
1186 * Cancels editing and reverts to original message.
1187 */
1188 _cancel: function() {
1189 var $container = this._container[this._activeElementID];
1190
1191 // remove ckEditor
1192 try {
1193 var $ckEditor = $('#' + this._messageEditorIDPrefix + $container.data('objectID')).ckeditorGet();
1194 $ckEditor.destroy();
1195 }
1196 catch (e) {
1197 // CKEditor might be not initialized yet, ignore
1198 }
1199
1200 // restore message
1201 var $messageBody = $container.find('.messageBody');
1202 $messageBody.children('.icon-spinner').remove();
1203 $messageBody.find('.messageText').html(this._cache);
1204
1205 // revert message options
1206 this._container[this._activeElementID].find('.messageOptions').removeClass('forceHidden');
1207
1208 this._activeElementID = '';
1209 },
1210
1211 /**
1212 * Handles successful AJAX calls.
1213 *
1214 * @param object data
1215 * @param string textStatus
1216 * @param jQuery jqXHR
1217 */
1218 _success: function(data, textStatus, jqXHR) {
1219 switch (data.returnValues.actionName) {
1220 case 'beginEdit':
1221 this._showEditor(data);
1222 break;
1223
1224 case 'save':
1225 this._showMessage(data);
1226 break;
1227 }
1228 },
1229
1230 /**
1231 * Shows WYSIWYG editor for active message.
1232 *
1233 * @param object data
1234 */
1235 _showEditor: function(data) {
1236 var $messageBody = this._container[this._activeElementID].find('.messageBody');
1237 $messageBody.children('.icon-spinner').remove();
1238 var $content = $messageBody.find('.messageText');
1239
1240 // insert wysiwyg
1241 $('' + data.returnValues.template).appendTo($content);
1242
1243 // bind buttons
1244 var $formSubmit = $content.find('.formSubmit');
1245 var $saveButton = $formSubmit.find('button[data-type=save]').click($.proxy(this._save, this));
1246 if (this._supportExtendedForm) $formSubmit.find('button[data-type=extended]').click($.proxy(this._prepareExtended, this));
1247 $formSubmit.find('button[data-type=cancel]').click($.proxy(this._cancel, this));
1248
1249 WCF.Message.Submit.registerButton(
1250 this._messageEditorIDPrefix + this._container[this._activeElementID].data('objectID'),
1251 $saveButton
1252 );
1253
1254 // hide message options
1255 this._container[this._activeElementID].find('.messageOptions').addClass('forceHidden');
1256
1257 new WCF.PeriodicalExecuter($.proxy(function(pe) {
1258 pe.stop();
1259
1260 $('#' + this._messageEditorIDPrefix + this._container[this._activeElementID].data('objectID')).ckeditorGet().ui.editor.focus();
1261 }, this), 250);
1262 },
1263
1264 /**
1265 * Reverts editor.
1266 */
1267 _revertEditor: function() {
1268 var $messageBody = this._container[this._activeElementID].find('.messageBody');
1269 $messageBody.children('span.icon-spinner').remove();
1270 $messageBody.find('.messageText').children().show();
1271 },
1272
1273 /**
1274 * Saves editor contents.
1275 */
1276 _save: function() {
1277 var $container = this._container[this._activeElementID];
1278 var $objectID = $container.data('objectID');
1279 var $message = '';
1280
1281 if ($.browser.mobile) {
1282 $message = $('#' + this._messageEditorIDPrefix + $objectID).val();
1283 }
1284 else {
1285 var $ckEditor = $('#' + this._messageEditorIDPrefix + $objectID).ckeditorGet();
1286 $message = $ckEditor.getData();
1287 }
1288
1289 this._proxy.setOption('data', {
1290 actionName: 'save',
1291 className: this._getClassName(),
1292 interfaceName: 'wcf\\data\\IMessageInlineEditorAction',
1293 parameters: {
1294 containerID: this._containerID,
1295 data: {
1296 message: $message
1297 },
1298 objectID: $objectID
1299 }
1300 });
1301 this._proxy.sendRequest();
1302
1303 this._hideEditor();
1304 },
1305
1306 /**
1307 * Prepares jumping to extended editing mode.
1308 */
1309 _prepareExtended: function() {
1310 var $container = this._container[this._activeElementID];
1311 var $objectID = $container.data('objectID');
1312 var $message = '';
1313
1314 if ($.browser.mobile) {
1315 $message = $('#' + this._messageEditorIDPrefix + $objectID).val();
1316 }
1317 else {
1318 var $ckEditor = $('#' + this._messageEditorIDPrefix + $objectID).ckeditorGet();
1319 $message = $ckEditor.getData();
1320 }
1321
1322 new WCF.Action.Proxy({
1323 autoSend: true,
1324 data: {
1325 actionName: 'jumpToExtended',
1326 className: this._getClassName(),
1327 parameters: {
1328 containerID: this._containerID,
1329 message: $message,
1330 messageID: $objectID
1331 }
1332 },
1333 success: function(data, textStatus, jqXHR) {
1334 window.location = data.returnValues.url;
1335 }
1336 });
1337 },
1338
1339 /**
1340 * Hides WYSIWYG editor.
1341 */
1342 _hideEditor: function() {
1343 var $messageBody = this._container[this._activeElementID].find('.messageBody');
1344 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody);
1345 $messageBody.find('.messageText').children().hide();
1346 },
1347
1348 /**
1349 * Shows rendered message.
1350 *
1351 * @param object data
1352 */
1353 _showMessage: function(data) {
1354 var $container = this._container[this._activeElementID];
1355 var $messageBody = $container.find('.messageBody');
1356 $messageBody.children('.icon-spinner').remove();
1357 var $content = $messageBody.find('.messageText');
1358
1359 // revert message options
1360 this._container[this._activeElementID].find('.messageOptions').removeClass('forceHidden');
1361
1362 // remove editor
1363 if (!$.browser.mobile) {
1364 var $ckEditor = $('#' + this._messageEditorIDPrefix + $container.data('objectID')).ckeditorGet();
1365 $ckEditor.destroy();
1366 }
1367
1368 $content.empty();
1369
1370 // insert new message
1371 $content.html(data.returnValues.message);
1372
1373 this._activeElementID = '';
1374
1375 this._updateHistory(this._getHash($container.data('objectID')));
1376
1377 this._notification.show();
1378 },
1379
1380 /**
1381 * Returns message action class name.
1382 *
1383 * @return string
1384 */
1385 _getClassName: function() {
1386 return '';
1387 },
1388
1389 /**
1390 * Returns the hash added to the url after successfully editing a message.
1391 *
1392 * @return string
1393 */
1394 _getHash: function(objectID) {
1395 return '#message' + objectID;
1396 },
1397
1398 /**
1399 * Updates the history to avoid old content when going back in the browser
1400 * history.
1401 *
1402 * @param hash
1403 */
1404 _updateHistory: function(hash) {
1405 window.location.hash = hash;
1406 }
1407 });
1408
1409 /**
1410 * Handles submit buttons for forms with an embedded WYSIWYG editor.
1411 */
1412 WCF.Message.Submit = {
1413 /**
1414 * list of registered buttons
1415 * @var object
1416 */
1417 _buttons: { },
1418
1419 /**
1420 * Registers submit button for specified wysiwyg container id.
1421 *
1422 * @param string wysiwygContainerID
1423 * @param string selector
1424 */
1425 registerButton: function(wysiwygContainerID, selector) {
1426 if (!WCF.Browser.isChrome()) {
1427 return;
1428 }
1429
1430 this._buttons[wysiwygContainerID] = $(selector);
1431 },
1432
1433 /**
1434 * Triggers 'click' event for registered buttons.
1435 */
1436 execute: function(wysiwygContainerID) {
1437 if (!this._buttons[wysiwygContainerID]) {
1438 return;
1439 }
1440
1441 this._buttons[wysiwygContainerID].trigger('click');
1442 }
1443 };
1444
1445 /**
1446 * Namespace for message quotes.
1447 */
1448 WCF.Message.Quote = { };
1449
1450 /**
1451 * Handles message quotes.
1452 *
1453 * @param string className
1454 * @param string objectType
1455 * @param string containerSelector
1456 * @param string messageBodySelector
1457 */
1458 WCF.Message.Quote.Handler = Class.extend({
1459 /**
1460 * active container id
1461 * @var string
1462 */
1463 _activeContainerID: '',
1464
1465 /**
1466 * action class name
1467 * @var string
1468 */
1469 _className: '',
1470
1471 /**
1472 * list of message containers
1473 * @var object
1474 */
1475 _containers: { },
1476
1477 /**
1478 * container selector
1479 * @var string
1480 */
1481 _containerSelector: '',
1482
1483 /**
1484 * 'copy quote' overlay
1485 * @var jQuery
1486 */
1487 _copyQuote: null,
1488
1489 /**
1490 * marked message
1491 * @var string
1492 */
1493 _message: '',
1494
1495 /**
1496 * message body selector
1497 * @var string
1498 */
1499 _messageBodySelector: '',
1500
1501 /**
1502 * object id
1503 * @var integer
1504 */
1505 _objectID: 0,
1506
1507 /**
1508 * object type name
1509 * @var string
1510 */
1511 _objectType: '',
1512
1513 /**
1514 * action proxy
1515 * @var WCF.Action.Proxy
1516 */
1517 _proxy: null,
1518
1519 /**
1520 * quote manager
1521 * @var WCF.Message.Quote.Manager
1522 */
1523 _quoteManager: null,
1524
1525 /**
1526 * Initializes the quote handler for given object type.
1527 *
1528 * @param WCF.Message.Quote.Manager quoteManager
1529 * @param string className
1530 * @param string objectType
1531 * @param string containerSelector
1532 * @param string messageBodySelector
1533 * @param string messageContentSelector
1534 */
1535 init: function(quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector) {
1536 this._className = className;
1537 if (this._className == '') {
1538 console.debug("[WCF.Message.QuoteManager] Empty class name given, aborting.");
1539 return;
1540 }
1541
1542 this._objectType = objectType;
1543 if (this._objectType == '') {
1544 console.debug("[WCF.Message.QuoteManager] Empty object type name given, aborting.");
1545 return;
1546 }
1547
1548 this._containerSelector = containerSelector;
1549 this._message = '';
1550 this._messageBodySelector = messageBodySelector;
1551 this._messageContentSelector = messageContentSelector;
1552 this._objectID = 0;
1553 this._proxy = new WCF.Action.Proxy({
1554 success: $.proxy(this._success, this)
1555 });
1556
1557 this._initContainers();
1558 this._initCopyQuote();
1559
1560 $(document).mouseup($.proxy(this._mouseUp, this));
1561
1562 // register with quote manager
1563 this._quoteManager = quoteManager;
1564 this._quoteManager.register(this._objectType, this);
1565
1566 // register with DOMNodeInsertedHandler
1567 WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.Quote.Handler' + objectType.hashCode(), $.proxy(this._initContainers, this));
1568 },
1569
1570 /**
1571 * Initializes message containers.
1572 */
1573 _initContainers: function() {
1574 var self = this;
1575 $(this._containerSelector).each(function(index, container) {
1576 var $container = $(container);
1577 var $containerID = $container.wcfIdentify();
1578
1579 if (!self._containers[$containerID]) {
1580 self._containers[$containerID] = $container;
1581 if ($container.hasClass('jsInvalidQuoteTarget')) {
1582 return true;
1583 }
1584
1585 if (self._messageBodySelector !== null) {
1586 $container = $container.find(self._messageBodySelector).data('containerID', $containerID);
1587 }
1588
1589 $container.mousedown($.proxy(self._mouseDown, self));
1590
1591 // bind event to quote whole message
1592 self._containers[$containerID].find('.jsQuoteMessage').click($.proxy(self._saveFullQuote, self));
1593 }
1594 });
1595 },
1596
1597 /**
1598 * Handles mouse down event.
1599 *
1600 * @param object event
1601 */
1602 _mouseDown: function(event) {
1603 // hide copy quote
1604 this._copyQuote.hide();
1605
1606 // store container ID
1607 var $container = $(event.currentTarget);
1608 if (this._messageBodySelector) {
1609 $container = this._containers[$container.data('containerID')];
1610 }
1611 this._activeContainerID = $container.wcfIdentify();
1612
1613 // remove alt-tag from all images, fixes quoting in Firefox
1614 if ($.browser.mozilla) {
1615 $container.find('img').each(function() {
1616 var $image = $(this);
1617 $image.data('__alt', $image.attr('alt')).removeAttr('alt');
1618 });
1619 }
1620 },
1621
1622 /**
1623 * Returns the text of a node and its children.
1624 *
1625 * @param object node
1626 * @return string
1627 */
1628 _getNodeText: function(node) {
1629 var nodeText = '';
1630
1631 for (var i = 0; i < node.childNodes.length; i++) {
1632 if (node.childNodes[i].nodeType == 3) {
1633 // text node
1634 nodeText += node.childNodes[i].nodeValue;
1635 }
1636 else {
1637 var $tagName = node.childNodes[i].tagName.toLowerCase();
1638 if ($tagName === 'li') {
1639 nodeText += "\r\n";
1640 }
1641
1642 nodeText += this._getNodeText(node.childNodes[i]);
1643
1644 if ($tagName === 'ul') {
1645 nodeText += "\n";
1646 }
1647 }
1648 }
1649
1650 return nodeText;
1651 },
1652
1653 /**
1654 * Handles the mouse up event.
1655 *
1656 * @param object event
1657 */
1658 _mouseUp: function(event) {
1659 // ignore event
1660 if (this._activeContainerID == '') {
1661 this._copyQuote.hide();
1662
1663 return;
1664 }
1665
1666 var $container = this._containers[this._activeContainerID];
1667 var $selection = this._getSelectedText();
1668 var $text = $.trim($selection);
1669 if ($text == '') {
1670 this._copyQuote.hide();
1671
1672 return;
1673 }
1674
1675 // compare selection with message text of given container
1676 var $messageText = null;
1677 if (this._messageBodySelector) {
1678 $messageText = this._getNodeText($container.find(this._messageContentSelector).get(0));
1679 }
1680 else {
1681 $messageText = this._getNodeText($container.get(0));
1682 }
1683
1684 // selected text is not part of $messageText or contains text from unrelated nodes
1685 if (this._normalize($messageText).indexOf(this._normalize($text)) === -1) {
1686 return;
1687 }
1688 this._copyQuote.show();
1689
1690 var $coordinates = this._getBoundingRectangle($selection);
1691 var $dimensions = this._copyQuote.getDimensions('outer');
1692 var $left = ($coordinates.right - $coordinates.left) / 2 - ($dimensions.width / 2) + $coordinates.left;
1693
1694 this._copyQuote.css({
1695 top: $coordinates.top - $dimensions.height - 7 + 'px',
1696 left: $left + 'px'
1697 });
1698 this._copyQuote.hide();
1699
1700 // reset containerID
1701 this._activeContainerID = '';
1702
1703 // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
1704 var self = this;
1705 new WCF.PeriodicalExecuter(function(pe) {
1706 pe.stop();
1707
1708 var $text = $.trim(self._getSelectedText());
1709 if ($text != '') {
1710 self._copyQuote.show();
1711 self._message = $text;
1712 self._objectID = $container.data('objectID');
1713
1714 // revert alt tags, fixes quoting in Firefox
1715 if ($.browser.mozilla) {
1716 $container.find('img').each(function() {
1717 var $image = $(this);
1718 $image.attr('alt', $image.data('__alt'));
1719 });
1720 }
1721 }
1722 }, 10);
1723 },
1724
1725 /**
1726 * Normalizes a text for comparison.
1727 *
1728 * @param string text
1729 * @return string
1730 */
1731 _normalize: function(text) {
1732 return text.replace(/\r?\n|\r/g, "\n").replace(/\s{2,}/g, ' ');
1733 },
1734
1735 /**
1736 * Returns the left or right offset of the current text selection.
1737 *
1738 * @param objct range
1739 * @param boolean before
1740 * @return object
1741 */
1742 _getOffset: function(range, before) {
1743 range.collapse(before);
1744
1745 var $elementID = WCF.getRandomID();
1746 var $element = document.createElement('span');
1747 $element.innerHTML = '<span id="' + $elementID + '"></span>';
1748 var $fragment = document.createDocumentFragment(), $node, $lastNode;
1749 while ($node = $element.firstChild) {
1750 $lastNode = $fragment.appendChild($node);
1751 }
1752 range.insertNode($fragment);
1753
1754 $element = $('#' + $elementID);
1755 var $position = $element.offset();
1756 $position.top = $position.top - $(window).scrollTop();
1757 $element.remove();
1758
1759 return $position;
1760 },
1761
1762 /**
1763 * Returns the offsets of the selection's bounding rectangle.
1764 *
1765 * @return object
1766 */
1767 _getBoundingRectangle: function(selection) {
1768 var $coordinates = null;
1769
1770 if (document.createRange && typeof document.createRange().getBoundingClientRect != "undefined") { // Opera, Firefox, Safari, Chrome
1771 if (selection.rangeCount > 0) {
1772 // the coordinates returned by getBoundingClientRect() is relative to the window, not the document!
1773 //var $rect = selection.getRangeAt(0).getBoundingClientRect();
1774 var $rects = selection.getRangeAt(0).getClientRects();
1775 if (!$.browser.mozilla && $rects.length > 1) {
1776 var $range = selection.getRangeAt(0);
1777 var $position1 = this._getOffset($range, true);
1778
1779 var $range = selection.getRangeAt(0);
1780 var $position2 = this._getOffset($range, false);
1781
1782 var $rect = {
1783 left: ($position1.left > $position2.left) ? $position2.left : $position1.left,
1784 right: ($position1.left > $position2.left) ? $position1.left : $position2.left,
1785 top: ($position1.top > $position2.top) ? $position2.top : $position1.top
1786 };
1787 }
1788 else {
1789 var $rect = selection.getRangeAt(0).getBoundingClientRect();
1790 }
1791
1792 var $document = $(document);
1793 var $offsetTop = $document.scrollTop();
1794
1795 $coordinates = {
1796 left: $rect.left,
1797 right: $rect.right,
1798 top: $rect.top + $offsetTop
1799 };
1800 }
1801 }
1802 else if (document.selection && document.selection.type != "Control") { // IE
1803 var $range = document.selection.createRange();
1804 // TODO: Check coordinates if they're relative too!
1805 $coordinates = {
1806 left: $range.boundingLeft,
1807 right: $range.boundingRight,
1808 top: $range.boundingTop
1809 };
1810 }
1811
1812 return $coordinates;
1813 },
1814
1815 /**
1816 * Initializes the 'copy quote' element.
1817 */
1818 _initCopyQuote: function() {
1819 this._copyQuote = $('#quoteManagerCopy');
1820 if (!this._copyQuote.length) {
1821 this._copyQuote = $('<div id="quoteManagerCopy" class="balloonTooltip"><span>' + WCF.Language.get('wcf.message.quote.quoteSelected') + '</span><span class="pointer"><span></span></span></div>').hide().appendTo(document.body);
1822 this._copyQuote.click($.proxy(this._saveQuote, this));
1823 }
1824 },
1825
1826 /**
1827 * Returns the text selection.
1828 *
1829 * @return object
1830 */
1831 _getSelectedText: function() {
1832 if (window.getSelection) { // Opera, Firefox, Safari, Chrome, IE 9+
1833 return window.getSelection();
1834 }
1835 else if (document.getSelection) { // Opera, Firefox, Safari, Chrome, IE 9+
1836 return document.getSelection();
1837 }
1838 else if (document.selection) { // IE 8
1839 return document.selection.createRange().text;
1840 }
1841
1842 return '';
1843 },
1844
1845 /**
1846 * Saves a full quote.
1847 *
1848 * @param object event
1849 */
1850 _saveFullQuote: function(event) {
1851 var $listItem = $(event.currentTarget);
1852
1853 this._proxy.setOption('data', {
1854 actionName: 'saveFullQuote',
1855 className: this._className,
1856 interfaceName: 'wcf\\data\\IMessageQuoteAction',
1857 objectIDs: [ $listItem.data('objectID') ]
1858 });
1859 this._proxy.sendRequest();
1860
1861 // mark element as quoted
1862 if ($listItem.data('isQuoted')) {
1863 $listItem.data('isQuoted', false).children('a').removeClass('active');
1864 }
1865 else {
1866 $listItem.data('isQuoted', true).children('a').addClass('active');
1867 }
1868
1869 // discard event
1870 event.stopPropagation();
1871 return false;
1872 },
1873
1874 /**
1875 * Saves a quote.
1876 */
1877 _saveQuote: function() {
1878 this._proxy.setOption('data', {
1879 actionName: 'saveQuote',
1880 className: this._className,
1881 interfaceName: 'wcf\\data\\IMessageQuoteAction',
1882 objectIDs: [ this._objectID ],
1883 parameters: {
1884 message: this._message
1885 }
1886 });
1887 this._proxy.sendRequest();
1888 },
1889
1890 /**
1891 * Handles successful AJAX requests.
1892 *
1893 * @param object data
1894 * @param string textStatus
1895 * @param jQuery jqXHR
1896 */
1897 _success: function(data, textStatus, jqXHR) {
1898 if (data.returnValues.count !== undefined) {
1899 var $fullQuoteObjectIDs = (data.fullQuoteObjectIDs !== undefined) ? data.fullQuoteObjectIDs : { };
1900 this._quoteManager.updateCount(data.returnValues.count, $fullQuoteObjectIDs);
1901 }
1902 },
1903
1904 /**
1905 * Updates the full quote data for all matching objects.
1906 *
1907 * @param array<integer> $objectIDs
1908 */
1909 updateFullQuoteObjectIDs: function(objectIDs) {
1910 for (var $containerID in this._containers) {
1911 this._containers[$containerID].find('.jsQuoteMessage').each(function(index, button) {
1912 // reset all markings
1913 var $button = $(button).data('isQuoted', 0);
1914 $button.children('a').removeClass('active');
1915
1916 // mark as active
1917 if (WCF.inArray($button.data('objectID'), objectIDs)) {
1918 $button.data('isQuoted', 1).children('a').addClass('active');
1919 }
1920 });
1921 }
1922 }
1923 });
1924
1925 /**
1926 * Manages stored quotes.
1927 *
1928 * @param integer count
1929 */
1930 WCF.Message.Quote.Manager = Class.extend({
1931 /**
1932 * list of form buttons
1933 * @var object
1934 */
1935 _buttons: { },
1936
1937 /**
1938 * ckEditor element
1939 * @var jQuery
1940 */
1941 _ckEditor: null,
1942
1943 /**
1944 * number of stored quotes
1945 * @var integer
1946 */
1947 _count: 0,
1948
1949 /**
1950 * dialog overlay
1951 * @var jQuery
1952 */
1953 _dialog: null,
1954
1955 /**
1956 * form element
1957 * @var jQuery
1958 */
1959 _form: null,
1960
1961 /**
1962 * list of quote handlers
1963 * @var object
1964 */
1965 _handlers: { },
1966
1967 /**
1968 * true, if an up-to-date template exists
1969 * @var boolean
1970 */
1971 _hasTemplate: false,
1972
1973 /**
1974 * true, if related quotes should be inserted
1975 * @var boolean
1976 */
1977 _insertQuotes: true,
1978
1979 /**
1980 * action proxy
1981 * @var WCF.Action.Proxy
1982 */
1983 _proxy: null,
1984
1985 /**
1986 * list of quotes to remove upon submit
1987 * @var array<string>
1988 */
1989 _removeOnSubmit: [ ],
1990
1991 /**
1992 * show quotes element
1993 * @var jQuery
1994 */
1995 _showQuotes: null,
1996
1997 /**
1998 * allow pasting
1999 * @var boolean
2000 */
2001 _supportPaste: false,
2002
2003 /**
2004 * Initializes the quote manager.
2005 *
2006 * @param integer count
2007 * @param string ckEditorID
2008 * @param boolean supportPaste
2009 * @param array<string> removeOnSubmit
2010 */
2011 init: function(count, ckEditorID, supportPaste, removeOnSubmit) {
2012 this._buttons = {
2013 insert: null,
2014 remove: null
2015 };
2016 this._ckEditor = null;
2017 this._count = parseInt(count) || 0;
2018 this._dialog = null;
2019 this._form = null;
2020 this._handlers = { };
2021 this._hasTemplate = false;
2022 this._insertQuotes = true;
2023 this._removeOnSubmit = [ ];
2024 this._showQuotes = null;
2025 this._supportPaste = false;
2026
2027 if (ckEditorID) {
2028 this._ckEditor = $('#' + ckEditorID);
2029 if (this._ckEditor.length) {
2030 this._supportPaste = true;
2031
2032 // get surrounding form-tag
2033 this._form = this._ckEditor.parents('form:eq(0)');
2034 if (this._form.length) {
2035 this._form.submit($.proxy(this._submit, this));
2036 this._removeOnSubmit = removeOnSubmit || [ ];
2037 }
2038 else {
2039 this._form = null;
2040
2041 // allow override
2042 this._supportPaste = (supportPaste === true) ? true : false;
2043 }
2044 }
2045 }
2046
2047 this._proxy = new WCF.Action.Proxy({
2048 showLoadingOverlay: false,
2049 success: $.proxy(this._success, this),
2050 url: 'index.php/MessageQuote/?t=' + SECURITY_TOKEN + SID_ARG_2ND
2051 });
2052
2053 this._toggleShowQuotes();
2054 },
2055
2056 /**
2057 * Registers a quote handler.
2058 *
2059 * @param string objectType
2060 * @param WCF.Message.Quote.Handler handler
2061 */
2062 register: function(objectType, handler) {
2063 this._handlers[objectType] = handler;
2064 },
2065
2066 /**
2067 * Updates number of stored quotes.
2068 *
2069 * @param integer count
2070 * @param object fullQuoteObjectIDs
2071 */
2072 updateCount: function(count, fullQuoteObjectIDs) {
2073 this._count = parseInt(count) || 0;
2074
2075 this._toggleShowQuotes();
2076
2077 // update full quote ids of handlers
2078 for (var $objectType in this._handlers) {
2079 if (fullQuoteObjectIDs[$objectType]) {
2080 this._handlers[$objectType].updateFullQuoteObjectIDs(fullQuoteObjectIDs[$objectType]);
2081 }
2082 }
2083 },
2084
2085 /**
2086 * Inserts all associated quotes upon first time using quick reply.
2087 *
2088 * @param string className
2089 * @param integer parentObjectID
2090 * @param object callback
2091 */
2092 insertQuotes: function(className, parentObjectID, callback) {
2093 if (!this._insertQuotes) {
2094 this._insertQuotes = true;
2095
2096 return;
2097 }
2098
2099 new WCF.Action.Proxy({
2100 autoSend: true,
2101 data: {
2102 actionName: 'getRenderedQuotes',
2103 className: className,
2104 interfaceName: 'wcf\\data\\IMessageQuoteAction',
2105 parameters: {
2106 parentObjectID: parentObjectID
2107 }
2108 },
2109 success: callback
2110 });
2111 },
2112
2113 /**
2114 * Toggles the display of the 'Show quotes' button
2115 */
2116 _toggleShowQuotes: function() {
2117 if (!this._count) {
2118 if (this._showQuotes !== null) {
2119 this._showQuotes.hide();
2120 }
2121 }
2122 else {
2123 if (this._showQuotes === null) {
2124 this._showQuotes = $('#showQuotes');
2125 if (!this._showQuotes.length) {
2126 this._showQuotes = $('<div id="showQuotes" class="balloonTooltip" />').click($.proxy(this._click, this)).appendTo(document.body);
2127 }
2128 }
2129
2130 var $text = WCF.Language.get('wcf.message.quote.showQuotes').replace(/#count#/, this._count);
2131 this._showQuotes.text($text).show();
2132 }
2133
2134 this._hasTemplate = false;
2135 },
2136
2137 /**
2138 * Handles clicks on 'Show quotes'.
2139 */
2140 _click: function() {
2141 if (this._hasTemplate) {
2142 this._dialog.wcfDialog('open');
2143 }
2144 else {
2145 this._proxy.showLoadingOverlayOnce();
2146
2147 this._proxy.setOption('data', {
2148 actionName: 'getQuotes',
2149 supportPaste: this._supportPaste
2150 });
2151 this._proxy.sendRequest();
2152 }
2153 },
2154
2155 /**
2156 * Renders the dialog.
2157 *
2158 * @param string template
2159 */
2160 renderDialog: function(template) {
2161 // create dialog if not exists
2162 if (this._dialog === null) {
2163 this._dialog = $('#messageQuoteList');
2164 if (!this._dialog.length) {
2165 this._dialog = $('<div id="messageQuoteList" />').hide().appendTo(document.body);
2166 }
2167 }
2168
2169 // add template
2170 this._dialog.html(template);
2171
2172 // add 'insert' and 'delete' buttons
2173 var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog);
2174 if (this._supportPaste) this._buttons.insert = $('<button>' + WCF.Language.get('wcf.message.quote.insertAllQuotes') + '</button>').click($.proxy(this._insertSelected, this)).appendTo($formSubmit);
2175 this._buttons.remove = $('<button>' + WCF.Language.get('wcf.message.quote.removeAllQuotes') + '</button>').click($.proxy(this._removeSelected, this)).appendTo($formSubmit);
2176
2177 // show dialog
2178 this._dialog.wcfDialog({
2179 title: WCF.Language.get('wcf.message.quote.manageQuotes')
2180 });
2181 this._dialog.wcfDialog('render');
2182 this._hasTemplate = true;
2183
2184 // bind event listener
2185 var $insertQuoteButtons = this._dialog.find('.jsInsertQuote');
2186 if (this._supportPaste) {
2187 $insertQuoteButtons.click($.proxy(this._insertQuote, this));
2188 }
2189 else {
2190 $insertQuoteButtons.hide();
2191 }
2192
2193 this._dialog.find('input.jsCheckbox').change($.proxy(this._changeButtons, this));
2194
2195 // mark quotes for removal
2196 // TODO: is this still supported?
2197 if (this._removeOnSubmit.length) {
2198 var self = this;
2199 this._dialog.find('input.jsRemoveQuote').each(function(index, input) {
2200 var $input = $(input).change($.proxy(this._change, this));
2201
2202 // mark for deletion
2203 if (WCF.inArray($input.parent('li').attr('data-quote-id'), self._removeOnSubmit)) {
2204 $input.attr('checked', 'checked');
2205 }
2206 });
2207 }
2208 },
2209
2210 /**
2211 * Updates button labels if a checkbox is checked or unchecked.
2212 */
2213 _changeButtons: function() {
2214 // selection
2215 if (this._dialog.find('input.jsCheckbox:checked').length) {
2216 if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertSelectedQuotes'));
2217 this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeSelectedQuotes'));
2218 }
2219 else {
2220 // no selection, pick all
2221 if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertAllQuotes'));
2222 this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeAllQuotes'));
2223 }
2224 },
2225
2226 /**
2227 * Checks for change event on delete-checkboxes.
2228 *
2229 * @param object event
2230 */
2231 _change: function(event) {
2232 var $input = $(event.currentTarget);
2233 var $quoteID = $input.parent('li').attr('data-quote-id');
2234
2235 if ($input.prop('checked')) {
2236 this._removeOnSubmit.push($quoteID);
2237 }
2238 else {
2239 for (var $index in this._removeOnSubmit) {
2240 if (this._removeOnSubmit[$index] == $quoteID) {
2241 delete this._removeOnSubmit[$index];
2242 break;
2243 }
2244 }
2245 }
2246 },
2247
2248 /**
2249 * Inserts the selected quotes.
2250 */
2251 _insertSelected: function() {
2252 var $api = $('.jsQuickReply:eq(0)').data('__api');
2253 if ($api && !$api.getContainer().is(':visible')) {
2254 this._insertQuotes = false;
2255 $api.click(null);
2256 }
2257
2258 if (!this._dialog.find('input.jsCheckbox:checked').length) {
2259 this._dialog.find('input.jsCheckbox').prop('checked', 'checked');
2260 }
2261
2262 // insert all quotes
2263 this._dialog.find('input.jsCheckbox:checked').each($.proxy(function(index, input) {
2264 this._insertQuote(null, input);
2265 }, this));
2266
2267 // close dialog
2268 this._dialog.wcfDialog('close');
2269 },
2270
2271 /**
2272 * Inserts a quote.
2273 *
2274 * @param object event
2275 * @param object inputElement
2276 */
2277 _insertQuote: function(event, inputElement) {
2278 if (event !== null) {
2279 var $api = $('.jsQuickReply:eq(0)').data('__api');
2280 if ($api && !$api.getContainer().is(':visible')) {
2281 this._insertQuotes = false;
2282 $api.click(null);
2283 }
2284 }
2285
2286 var $listItem = (event === null) ? $(inputElement).parents('li') : $(event.currentTarget).parents('li');
2287 var $quote = $.trim($listItem.children('div.jsFullQuote').text());
2288 var $message = $listItem.parents('article.message');
2289
2290 // build quote tag
2291 $quote = "[quote='" + $message.attr('data-username') + "','" + $message.data('link') + "']" + $quote + "[/quote]";
2292
2293 // insert into ckEditor
2294 var $ckEditor = ($.browser.mobile) ? null : this._ckEditor.ckeditorGet();
2295 if ($ckEditor !== null && $ckEditor.mode === 'wysiwyg') {
2296 // in design mode
2297 $ckEditor.insertText($quote + "\n\n");
2298 }
2299 else {
2300 // in source mode
2301 var $textarea = ($.browser.mobile) ? this._ckEditor : this._ckEditor.next('.cke_editor_text').find('textarea');
2302 var $value = $textarea.val();
2303 $quote += "\n\n";
2304 if ($value.length == 0) {
2305 $textarea.val($quote);
2306 }
2307 else {
2308 var $position = $textarea.getCaret();
2309 $textarea.val( $value.substr(0, $position) + $quote + $value.substr($position) );
2310 }
2311 }
2312
2313 // remove quote upon submit or upon request
2314 this._removeOnSubmit.push($listItem.attr('data-quote-id'));
2315
2316 // close dialog
2317 if (event !== null) {
2318 this._dialog.wcfDialog('close');
2319 }
2320 },
2321
2322 /**
2323 * Removes selected quotes.
2324 */
2325 _removeSelected: function() {
2326 if (!this._dialog.find('input.jsCheckbox:checked').length) {
2327 this._dialog.find('input.jsCheckbox').prop('checked', 'checked');
2328 }
2329
2330 var $quoteIDs = [ ];
2331 this._dialog.find('input.jsCheckbox:checked').each(function(index, input) {
2332 $quoteIDs.push($(input).parents('li').attr('data-quote-id'));
2333 });
2334
2335 if ($quoteIDs.length) {
2336 // get object types
2337 var $objectTypes = [ ];
2338 for (var $objectType in this._handlers) {
2339 $objectTypes.push($objectType);
2340 }
2341
2342 this._proxy.setOption('data', {
2343 actionName: 'remove',
2344 objectTypes: $objectTypes,
2345 quoteIDs: $quoteIDs
2346 });
2347 this._proxy.sendRequest();
2348
2349 this._dialog.wcfDialog('close');
2350 }
2351 },
2352
2353 /**
2354 * Appends list of quote ids to remove after successful submit.
2355 */
2356 _submit: function() {
2357 if (this._supportPaste && this._removeOnSubmit.length > 0) {
2358 var $formSubmit = this._form.find('.formSubmit');
2359 for (var $i in this._removeOnSubmit) {
2360 $('<input type="hidden" name="__removeQuoteIDs[]" value="' + this._removeOnSubmit[$i] + '" />').appendTo($formSubmit);
2361 }
2362 }
2363 },
2364
2365 /**
2366 * Returns a list of quote ids marked for removal.
2367 *
2368 * @return array<integer>
2369 */
2370 getQuotesMarkedForRemoval: function() {
2371 return this._removeOnSubmit;
2372 },
2373
2374 /**
2375 * Marks quote ids for removal.
2376 */
2377 markQuotesForRemoval: function() {
2378 if (this._removeOnSubmit.length) {
2379 this._proxy.setOption('data', {
2380 actionName: 'markForRemoval',
2381 quoteIDs: this._removeOnSubmit
2382 });
2383 this._proxy.sendRequest();
2384 }
2385 },
2386
2387 /**
2388 * Remoes all marked quote ids.
2389 */
2390 removeMarkedQuotes: function() {
2391 if (this._removeOnSubmit.length) {
2392 this._proxy.setOption('data', {
2393 actionName: 'removeMarkedQuotes'
2394 });
2395 this._proxy.sendRequest();
2396 }
2397 },
2398
2399 /**
2400 * Counts stored quotes.
2401 */
2402 countQuotes: function() {
2403 var $objectTypes = [ ];
2404 for (var $objectType in this._handlers) {
2405 $objectTypes.push($objectType);
2406 }
2407
2408 this._proxy.setOption('data', {
2409 actionName: 'count',
2410 objectTypes: $objectTypes
2411 });
2412 this._proxy.sendRequest();
2413 },
2414
2415 /**
2416 * Handles successful AJAX requests.
2417 *
2418 * @param object data
2419 * @param string textStatus
2420 * @param jQuery jqXHR
2421 */
2422 _success: function(data, textStatus, jqXHR) {
2423 if (data === null) {
2424 return;
2425 }
2426
2427 if (data.count !== undefined) {
2428 var $fullQuoteObjectIDs = (data.fullQuoteObjectIDs !== undefined) ? data.fullQuoteObjectIDs : { };
2429 this.updateCount(data.count, $fullQuoteObjectIDs);
2430 }
2431
2432 if (data.template !== undefined) {
2433 if ($.trim(data.template) == '') {
2434 this.updateCount(0, { });
2435 }
2436 else {
2437 this.renderDialog(data.template);
2438 }
2439 }
2440 }
2441 });
2442
2443 /**
2444 * Namespace for message sharing related classes.
2445 */
2446 WCF.Message.Share = { };
2447
2448 /**
2449 * Displays a dialog overlay for permalinks.
2450 */
2451 WCF.Message.Share.Content = Class.extend({
2452 /**
2453 * list of cached templates
2454 * @var object
2455 */
2456 _cache: { },
2457
2458 /**
2459 * dialog overlay
2460 * @var jQuery
2461 */
2462 _dialog: null,
2463
2464 /**
2465 * Initializes the WCF.Message.Share.Content class.
2466 */
2467 init: function() {
2468 this._cache = { };
2469 this._dialog = null;
2470
2471 this._initLinks();
2472
2473 WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.Share.Content', $.proxy(this._initLinks, this));
2474 },
2475
2476 /**
2477 * Initializes share links.
2478 */
2479 _initLinks: function() {
2480 $('a.jsButtonShare').removeClass('jsButtonShare').click($.proxy(this._click, this));
2481 },
2482
2483 /**
2484 * Displays links to share this content.
2485 *
2486 * @param object event
2487 */
2488 _click: function(event) {
2489 event.preventDefault();
2490
2491 var $target = $(event.currentTarget);
2492 var $link = $target.prop('href');
2493 var $title = ($target.data('linkTitle') ? $target.data('linkTitle') : $link);
2494 var $key = $link.hashCode();
2495 if (this._cache[$key] === undefined) {
2496
2497 // remove dialog contents
2498 var $dialogInitialized = false;
2499 if (this._dialog === null) {
2500 this._dialog = $('<div />').hide().appendTo(document.body);
2501 $dialogInitialized = true;
2502 }
2503 else {
2504 this._dialog.empty();
2505 }
2506
2507 // permalink (plain text)
2508 var $fieldset = $('<fieldset><legend><label for="__sharePermalink">' + WCF.Language.get('wcf.message.share.permalink') + '</label></legend></fieldset>').appendTo(this._dialog);
2509 $('<input type="text" id="__sharePermalink" class="long" readonly="readonly" />').attr('value', $link).appendTo($fieldset);
2510
2511 // permalink (BBCode)
2512 var $fieldset = $('<fieldset><legend><label for="__sharePermalinkBBCode">' + WCF.Language.get('wcf.message.share.permalink.bbcode') + '</label></legend></fieldset>').appendTo(this._dialog);
2513 $('<input type="text" id="__sharePermalinkBBCode" class="long" readonly="readonly" />').attr('value', '[url=\'' + $link + '\']' + $title + '[/url]').appendTo($fieldset);
2514
2515 // permalink (HTML)
2516 var $fieldset = $('<fieldset><legend><label for="__sharePermalinkHTML">' + WCF.Language.get('wcf.message.share.permalink.html') + '</label></legend></fieldset>').appendTo(this._dialog);
2517 $('<input type="text" id="__sharePermalinkHTML" class="long" readonly="readonly" />').attr('value', '<a href="' + $link + '">' + WCF.String.escapeHTML($title) + '</a>').appendTo($fieldset);
2518
2519 this._cache[$key] = this._dialog.html();
2520
2521 if ($dialogInitialized) {
2522 this._dialog.wcfDialog({
2523 title: WCF.Language.get('wcf.message.share')
2524 });
2525 }
2526 else {
2527 this._dialog.wcfDialog('open');
2528 }
2529 }
2530 else {
2531
2532 this._dialog.html(this._cache[$key]).wcfDialog('open');
2533 }
2534
2535 this._dialog.find('input').click(function() { $(this).select(); });
2536 }
2537 });
2538
2539 /**
2540 * Provides buttons to share a page through multiple social community sites.
2541 *
2542 * @param boolean fetchObjectCount
2543 */
2544 WCF.Message.Share.Page = Class.extend({
2545 /**
2546 * list of share buttons
2547 * @var object
2548 */
2549 _ui: { },
2550
2551 /**
2552 * page description
2553 * @var string
2554 */
2555 _pageDescription: '',
2556
2557 /**
2558 * canonical page URL
2559 * @var string
2560 */
2561 _pageURL: '',
2562
2563 /**
2564 * Initializes the WCF.Message.Share.Page class.
2565 *
2566 * @param boolean fetchObjectCount
2567 */
2568 init: function(fetchObjectCount) {
2569 this._pageDescription = encodeURIComponent($('meta[property="og:description"]').prop('content'));
2570 this._pageURL = encodeURIComponent($('meta[property="og:url"]').prop('content'));
2571
2572 var $container = $('.messageShareButtons');
2573 this._ui = {
2574 facebook: $container.find('.jsShareFacebook'),
2575 google: $container.find('.jsShareGoogle'),
2576 reddit: $container.find('.jsShareReddit'),
2577 twitter: $container.find('.jsShareTwitter')
2578 };
2579
2580 this._ui.facebook.children('a').click($.proxy(this._shareFacebook, this));
2581 this._ui.google.children('a').click($.proxy(this._shareGoogle, this));
2582 this._ui.reddit.children('a').click($.proxy(this._shareReddit, this));
2583 this._ui.twitter.children('a').click($.proxy(this._shareTwitter, this));
2584
2585 if (fetchObjectCount === true) {
2586 this._fetchFacebook();
2587 this._fetchTwitter();
2588 this._fetchReddit();
2589 }
2590 },
2591
2592 /**
2593 * Shares current page to selected social community site.
2594 *
2595 * @param string objectName
2596 * @param string url
2597 */
2598 _share: function(objectName, url) {
2599 window.open(url.replace(/{pageURL}/, this._pageURL).replace(/{text}/, this._pageDescription), 'height=600,width=600');
2600 },
2601
2602 /**
2603 * Shares current page with Facebook.
2604 */
2605 _shareFacebook: function() {
2606 this._share('facebook', 'https://www.facebook.com/sharer.php?u={pageURL}&t={text}');
2607 },
2608
2609 /**
2610 * Shares current page with Google Plus.
2611 */
2612 _shareGoogle: function() {
2613 this._share('google', 'https://plus.google.com/share?url={pageURL}');
2614 },
2615
2616 /**
2617 * Shares current page with Reddit.
2618 */
2619 _shareReddit: function() {
2620 this._share('reddit', 'https://ssl.reddit.com/submit?url={pageURL}');
2621 },
2622
2623 /**
2624 * Shares current page with Twitter.
2625 */
2626 _shareTwitter: function() {
2627 this._share('twitter', 'https://twitter.com/share?url={pageURL}&text={text}');
2628 },
2629
2630 /**
2631 * Fetches share count from a social community site.
2632 *
2633 * @param string url
2634 * @param object callback
2635 * @param string callbackName
2636 */
2637 _fetchCount: function(url, callback, callbackName) {
2638 var $options = {
2639 autoSend: true,
2640 dataType: 'jsonp',
2641 showLoadingOverlay: false,
2642 success: callback,
2643 suppressErrors: true,
2644 type: 'GET',
2645 url: url.replace(/{pageURL}/, this._pageURL)
2646 };
2647 if (callbackName) {
2648 $options.jsonp = callbackName;
2649 }
2650
2651 new WCF.Action.Proxy($options);
2652 },
2653
2654 /**
2655 * Fetches number of Facebook likes.
2656 */
2657 _fetchFacebook: function() {
2658 this._fetchCount('https://graph.facebook.com/?id={pageURL}', $.proxy(function(data) {
2659 if (data.shares) {
2660 this._ui.facebook.children('span.badge').show().text(data.shares);
2661 }
2662 }, this));
2663 },
2664
2665 /**
2666 * Fetches tweet count from Twitter.
2667 */
2668 _fetchTwitter: function() {
2669 this._fetchCount('http://urls.api.twitter.com/1/urls/count.json?url={pageURL}', $.proxy(function(data) {
2670 if (data.count) {
2671 this._ui.twitter.children('span.badge').show().text(data.count);
2672 }
2673 }, this));
2674 },
2675
2676 /**
2677 * Fetches cumulative vote sum from Reddit.
2678 */
2679 _fetchReddit: function() {
2680 this._fetchCount('http://www.reddit.com/api/info.json?url={pageURL}', $.proxy(function(data) {
2681 if (data.data.children.length) {
2682 this._ui.reddit.children('span.badge').show().text(data.data.children[0].data.score);
2683 }
2684 }, this), 'jsonp');
2685 }
2686 });