2 * Message related classes for WCF
4 * @author Alexander Ebert
5 * @copyright 2001-2014 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
11 * Namespace for BBCode related classes.
13 WCF
.Message
.BBCode
= { };
16 * BBCode Viewer for WCF.
18 WCF
.Message
.BBCode
.CodeViewer
= Class
.extend({
26 * Initializes the WCF.Message.BBCode.CodeViewer class.
31 this._initCodeBoxes();
33 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.BBCode.CodeViewer', $.proxy(this._initCodeBoxes
, this));
34 WCF
.DOMNodeInsertedHandler
.execute();
38 * Initializes available code boxes.
40 _initCodeBoxes: function() {
41 $('.codeBox:not(.jsCodeViewer)').each($.proxy(function(index
, codeBox
) {
42 var $codeBox
= $(codeBox
).addClass('jsCodeViewer');
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));
49 * Shows a code viewer for a specific code box.
53 _click: function(event
) {
55 $(event
.currentTarget
).parents('div').next('ol').children('li').each(function(index
, listItem
) {
60 // do *not* use $.trim here, as we want to preserve whitespaces
61 $content
+= $(listItem
).text().replace(/\n+$/, '');
64 if (this._dialog
=== null) {
65 this._dialog
= $('<div><textarea cols="60" rows="12" readonly="readonly" /></div>').hide().appendTo(document
.body
);
66 this._dialog
.children('textarea').val($content
);
67 this._dialog
.wcfDialog({
68 title
: WCF
.Language
.get('wcf.message.bbcode.code.copy')
72 this._dialog
.children('textarea').val($content
);
73 this._dialog
.wcfDialog('open');
76 this._dialog
.children('textarea').select();
81 * Prevents multiple submits of the same form by disabling the submit button.
83 WCF
.Message
.FormGuard
= Class
.extend({
85 * Initializes the WCF.Message.FormGuard class.
88 var $forms
= $('form.jsFormGuard').removeClass('jsFormGuard').submit(function() {
89 $(this).find('.formSubmit input[type=submit]').disable();
92 // restore buttons, prevents disabled buttons on back navigation in Opera
93 $(window
).unload(function() {
94 $forms
.find('.formSubmit input[type=submit]').enable();
100 * Provides previews for ckEditor message fields.
102 * @param string className
103 * @param string messageFieldID
104 * @param string previewButtonID
106 WCF
.Message
.Preview
= Class
.extend({
127 * @var WCF.Action.Proxy
135 _previewButton
: null,
138 * previous button label
141 _previewButtonLabel
: '',
144 * Initializes a new WCF.Message.Preview object.
146 * @param string className
147 * @param string messageFieldID
148 * @param string previewButtonID
150 init: function(className
, messageFieldID
, previewButtonID
) {
151 this._className
= className
;
153 // validate message field
154 this._messageFieldID
= $.wcfEscapeID(messageFieldID
);
155 this._messageField
= $('#' + this._messageFieldID
);
156 if (!this._messageField
.length
) {
157 console
.debug("[WCF.Message.Preview] Unable to find message field identified by '" + this._messageFieldID
+ "'");
161 // validate preview button
162 previewButtonID
= $.wcfEscapeID(previewButtonID
);
163 this._previewButton
= $('#' + previewButtonID
);
164 if (!this._previewButton
.length
) {
165 console
.debug("[WCF.Message.Preview] Unable to find preview button identified by '" + previewButtonID
+ "'");
169 this._previewButton
.click($.proxy(this._click
, this));
170 this._proxy
= new WCF
.Action
.Proxy({
171 failure
: $.proxy(this._failure
, this),
172 success
: $.proxy(this._success
, this)
177 * Reads message field input and triggers an AJAX request.
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
+ "'");
186 this._proxy
.setOption('data', {
187 actionName
: 'getMessagePreview',
188 className
: this._className
,
189 parameters
: this._getParameters($message
)
191 this._proxy
.sendRequest();
193 // update button label
194 this._previewButtonLabel
= this._previewButton
.html();
195 this._previewButton
.html(WCF
.Language
.get('wcf.global.loading')).disable();
198 event
.stopPropagation();
203 * Returns request parameters.
205 * @param string message
208 _getParameters: function(message
) {
209 // collect message form 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');
228 * Returns parsed message from ckEditor or null if editor was not accessible.
232 _getMessage: function() {
233 if (!$.browser
.ckeditor
) {
234 return this._messageField
.val();
236 else if (this._messageField
.data('ckeditorInstance')) {
237 var $ckEditor
= this._messageField
.ckeditorGet();
238 return $ckEditor
.getData();
245 * Handles successful AJAX requests.
248 * @param string textStatus
249 * @param jQuery jqXHR
251 _success: function(data
, textStatus
, jqXHR
) {
252 // restore preview button
253 this._previewButton
.html(this._previewButtonLabel
).enable();
255 // remove error message
256 this._messageField
.parent().children('small.innerError').remove();
259 this._handleResponse(data
);
263 * Evaluates response data.
267 _handleResponse: function(data
) { },
270 * Handles errors during preview requests.
272 * The return values indicates if the default error overlay is shown.
277 _failure: function(data
) {
278 if (data
=== null || data
.returnValues
=== undefined || data
.returnValues
.errorType
=== undefined) {
282 // restore preview button
283 this._previewButton
.html(this._previewButtonLabel
).enable();
285 var $innerError
= this._messageField
.next('small.innerError').empty();
286 if (!$innerError
.length
) {
287 $innerError
= $('<small class="innerError" />').appendTo(this._messageField
.parent());
290 $innerError
.html(data
.returnValues
.errorType
);
297 * Default implementation for message previews.
299 * @see WCF.Message.Preview
301 WCF
.Message
.DefaultPreview
= WCF
.Message
.Preview
.extend({
302 _attachmentObjectType
: null,
303 _attachmentObjectID
: null,
307 * @see WCF.Message.Preview.init()
309 init: function(attachmentObjectType
, attachmentObjectID
, tmpHash
) {
310 this._super('wcf\\data\\bbcode\\MessagePreviewAction', 'text', 'previewButton');
312 this._attachmentObjectType
= attachmentObjectType
|| null;
313 this._attachmentObjectID
= attachmentObjectID
|| null;
314 this._tmpHash
= tmpHash
|| null;
318 * @see WCF.Message.Preview._handleResponse()
320 _handleResponse: function(data
) {
321 var $preview
= $('#previewContainer');
322 if (!$preview
.length
) {
323 $preview
= $('<div class="container containerPadding marginTop" id="previewContainer"><fieldset><legend>' + WCF
.Language
.get('wcf.global.preview') + '</legend><div></div></fieldset>').prependTo($('#messageContainer')).wcfFadeIn();
326 $preview
.find('div:eq(0)').html(data
.returnValues
.message
);
328 new WCF
.Effect
.Scroll().scrollTo($preview
);
332 * @see WCF.Message.Preview._getParameters()
334 _getParameters: function(message
) {
335 var $parameters
= this._super(message
);
337 if (this._attachmentObjectType
!= null) {
338 $parameters
.attachmentObjectType
= this._attachmentObjectType
;
339 $parameters
.attachmentObjectID
= this._attachmentObjectID
;
340 $parameters
.tmpHash
= this._tmpHash
;
348 * Handles multilingualism for messages.
350 * @param integer languageID
351 * @param object availableLanguages
352 * @param boolean forceSelection
354 WCF
.Message
.Multilingualism
= Class
.extend({
356 * list of available languages
359 _availableLanguages
: { },
368 * language input element
371 _languageInput
: null,
374 * Initializes WCF.Message.Multilingualism
376 * @param integer languageID
377 * @param object availableLanguages
378 * @param boolean forceSelection
380 init: function(languageID
, availableLanguages
, forceSelection
) {
381 this._availableLanguages
= availableLanguages
;
382 this._languageID
= languageID
|| 0;
384 this._languageInput
= $('#languageID');
386 // preselect current language id
389 // register event listener
390 this._languageInput
.find('.dropdownMenu > li').click($.proxy(this._click
, this));
392 // add element to disable multilingualism
393 if (!forceSelection
) {
394 var $dropdownMenu
= this._languageInput
.find('.dropdownMenu');
395 $('<li class="dropdownDivider" />').appendTo($dropdownMenu
);
396 $('<li><span><span class="badge">' + this._availableLanguages
[0] + '</span></span></li>').click($.proxy(this._disable
, this)).appendTo($dropdownMenu
);
400 this._languageInput
.parents('form').submit($.proxy(this._submit
, this));
404 * Handles language selections.
406 * @param object event
408 _click: function(event
) {
409 this._languageID
= $(event
.currentTarget
).data('languageID');
414 * Disables language selection.
416 _disable: function() {
417 this._languageID
= 0;
422 * Updates selected language.
424 _updateLabel: function() {
425 this._languageInput
.find('.dropdownToggle > span').text(this._availableLanguages
[this._languageID
]);
429 * Sets language id upon submit.
431 _submit: function() {
432 this._languageInput
.next('input[name=languageID]').prop('value', this._languageID
);
437 * Loads smiley categories upon user request.
439 WCF
.Message
.SmileyCategories
= Class
.extend({
441 * list of already loaded category ids
442 * @var array<integer>
448 * @var WCF.Action.Proxy
459 * Initializes the smiley loader.
461 * @param string ckEditorID
465 this._proxy
= new WCF
.Action
.Proxy({
466 success
: $.proxy(this._success
, this)
469 $('#smilies').on('wcftabsbeforeactivate', $.proxy(this._click
, this));
473 new WCF
.PeriodicalExecuter(function(pe
) {
476 self
._click({ }, { newTab
: $('#smilies > .menu li.ui-state-active') });
481 * Handles tab menu clicks.
483 * @param object event
486 _click: function(event
, ui
) {
487 var $categoryID
= parseInt($(ui
.newTab
).children('a').data('smileyCategoryID'));
489 if ($categoryID
&& !WCF
.inArray($categoryID
, this._cache
)) {
490 this._proxy
.setOption('data', {
491 actionName
: 'getSmilies',
492 className
: 'wcf\\data\\smiley\\category\\SmileyCategoryAction',
493 objectIDs
: [ $categoryID
]
495 this._proxy
.sendRequest();
500 * Handles successful AJAX requests.
503 * @param string textStatus
504 * @param jQuery jqXHR
506 _success: function(data
, textStatus
, jqXHR
) {
507 var $categoryID
= parseInt(data
.returnValues
.smileyCategoryID
);
508 this._cache
.push($categoryID
);
510 $('#smilies-' + $categoryID
).html(data
.returnValues
.template
);
515 * Handles smiley clicks.
517 WCF
.Message
.Smilies
= Class
.extend({
525 * Initializes the smiley handler.
527 * @param string ckEditorID
529 init: function(ckEditorID
) {
532 this._ckEditor
= $('#' + ckEditorID
);
534 // add smiley click handler
535 $(document
).on('click', '.jsSmiley', $.proxy(this._smileyClick
, this));
540 * Handles tab smiley clicks.
542 * @param object event
544 _smileyClick: function(event
) {
545 var $target
= $(event
.currentTarget
);
546 var $smileyCode
= $target
.data('smileyCode');
547 var $smileyPath
= $target
.data('smileyPath');
550 var $ckEditor
= this._ckEditor
.ckeditorGet();
552 // add smiley to config
553 if (!WCF
.inArray($smileyCode
, $ckEditor
.config
.smiley_descriptions
)) {
554 $ckEditor
.config
.smiley_descriptions
.push($smileyCode
);
555 $ckEditor
.config
.smiley_images
.push($smileyPath
);
558 if ($ckEditor
.mode
=== 'wysiwyg') {
560 var $img
= $ckEditor
.document
.createElement('img', {
562 src
: $ckEditor
.config
.smiley_path
+ $smileyPath
,
567 $ckEditor
.insertText(' ');
568 $ckEditor
.insertElement($img
);
569 $ckEditor
.insertText(' ');
573 var $textarea
= this._ckEditor
.next('.cke_editor_text').find('textarea');
574 var $value
= $textarea
.val();
575 if ($value
.length
== 0) {
576 $textarea
.val($smileyCode
);
577 $textarea
.setCaret($smileyCode
.length
);
580 var $position
= $textarea
.getCaret();
581 var $string
= (($value
.substr($position
- 1, 1) !== ' ') ? ' ' : '') + $smileyCode
+ ' ';
582 $textarea
.val( $value
.substr(0, $position
) + $string
+ $value
.substr($position
) );
583 $textarea
.setCaret($position
+ $string
.length
);
590 * Provides an AJAX-based quick reply for messages.
592 WCF
.Message
.QuickReply
= Class
.extend({
594 * quick reply container
606 * notification object
607 * @var WCF.System.Notification
612 * true, if a request to save the message is pending
619 * @var WCF.Action.Proxy
624 * quote manager object
625 * @var WCF.Message.Quote.Manager
631 * @var WCF.Effect.Scroll
633 _scrollHandler
: null,
636 * success message for created but invisible messages
639 _successMessageNonVisible
: '',
642 * Initializes a new WCF.Message.QuickReply object.
644 * @param boolean supportExtendedForm
645 * @param WCF.Message.Quote.Manager quoteManager
647 init: function(supportExtendedForm
, quoteManager
) {
648 this._container
= $('#messageQuickReply');
649 this._container
.children('.message').addClass('jsInvalidQuoteTarget');
650 this._messageField
= $('#text');
651 this._pendingSave
= false;
652 if (!this._container
|| !this._messageField
) {
657 var $formSubmit
= this._container
.find('.formSubmit');
658 $formSubmit
.find('button[data-type=save]').click($.proxy(this._save
, this));
659 if (supportExtendedForm
) $formSubmit
.find('button[data-type=extended]').click($.proxy(this._prepareExtended
, this));
660 $formSubmit
.find('button[data-type=cancel]').click($.proxy(this._cancel
, this));
662 if (quoteManager
) this._quoteManager
= quoteManager
;
664 $('.jsQuickReply').data('__api', this).click($.proxy(this.click
, this));
666 this._proxy
= new WCF
.Action
.Proxy({
667 failure
: $.proxy(this._failure
, this),
668 showLoadingOverlay
: false,
669 success
: $.proxy(this._success
, this)
671 this._scroll
= new WCF
.Effect
.Scroll();
672 this._notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.global.success.add'));
673 this._successMessageNonVisible
= '';
677 * Handles clicks on reply button.
679 * @param object event
681 click: function(event
) {
682 this._container
.toggle();
684 if (this._container
.is(':visible')) {
685 this._scroll
.scrollTo(this._container
, true);
687 WCF
.Message
.Submit
.registerButton('text', this._container
.find('.formSubmit button[data-type=save]'));
689 if (this._quoteManager
) {
690 // check if message field is empty
692 if ($.browser
.ckeditor
) {
694 this._messageField
.ckeditor(function() {
695 $empty
= (!$.trim(this.getData()).length
);
696 self
._ckeditorCallback($empty
);
701 $empty
= (!this._messageField
.val().length
);
702 this._ckeditorCallback($empty
);
708 if (event
!== null) {
709 event
.stopPropagation();
714 _ckeditorCallback: function(isEmpty
) {
716 this._quoteManager
.insertQuotes(this._getClassName(), this._getObjectID(), $.proxy(this._insertQuotes
, this));
719 if ($.browser
.ckeditor
) {
720 this._messageField
.ckeditorGet().ui
.editor
.focus();
723 this._messageField
.focus();
728 * Returns container element.
732 getContainer: function() {
733 return this._container
;
737 * Insertes quotes into the quick reply editor.
741 _insertQuotes: function(data
) {
742 if (!data
.returnValues
.template
) {
746 if ($.browser
.ckeditor
) {
747 var $ckEditor
= this._messageField
.ckeditorGet();
749 // work-around for a strange selection bug in Firefox: http://www.woltlab.com/forum/index.php/Thread/220522-Zitat-Fehler/
750 if ($ckEditor
.getSelection().getStartElement() === null) {
751 // range is broken, set it to end of text: http://stackoverflow.com/a/16308194
752 var $range
= $ckEditor
.createRange();
753 $range
.moveToPosition($range
.root
, CKEDITOR
.POSITION_BEFORE_END
);
754 $ckEditor
.getSelection().selectRanges([ $range
]);
757 $ckEditor
.insertText(data
.returnValues
.template
);
760 this._messageField
.val(data
.returnValues
.template
);
768 if (this._pendingSave
) {
774 if ($.browser
.ckeditor
) {
775 var $ckEditor
= this._messageField
.ckeditorGet();
776 $message
= $.trim($ckEditor
.getData());
779 $message
= $.trim(this._messageField
.val());
782 // check if message is empty
783 var $innerError
= this._messageField
.parent().find('small.innerError');
784 if ($message
=== '' || $message
=== '0') {
785 if (!$innerError
.length
) {
786 $innerError
= $('<small class="innerError" />').appendTo(this._messageField
.parent());
789 $innerError
.html(WCF
.Language
.get('wcf.global.form.error.empty'));
793 $innerError
.remove();
796 this._pendingSave
= true;
798 this._proxy
.setOption('data', {
799 actionName
: 'quickReply',
800 className
: this._getClassName(),
801 interfaceName
: 'wcf\\data\\IMessageQuickReplyAction',
802 parameters
: this._getParameters($message
)
804 this._proxy
.sendRequest();
806 // show spinner and hide CKEditor
807 var $messageBody
= this._container
.find('.messageQuickReplyContent .messageBody');
808 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody
);
809 $messageBody
.children('#cke_text').hide().end().next().hide();
813 * Returns the parameters for the save request.
815 * @param string message
818 _getParameters: function(message
) {
820 objectID
: this._getObjectID(),
824 lastPostTime
: this._container
.data('lastPostTime'),
825 pageNo
: this._container
.data('pageNo'),
826 removeQuoteIDs
: (this._quoteManager
=== null ? [ ] : this._quoteManager
.getQuotesMarkedForRemoval())
828 if (this._container
.data('anchor')) {
829 $parameters
.anchor
= this._container
.data('anchor');
836 * Cancels quick reply.
838 _cancel: function() {
839 this._revertQuickReply(true);
841 if ($.browser
.ckeditor
) {
843 this._messageField
.ckeditorGet().setData('');
846 this._messageField
.val('');
851 * Reverts quick reply to original state and optionally hiding it.
853 * @param boolean hide
855 _revertQuickReply: function(hide
) {
856 var $messageBody
= this._container
.find('.messageQuickReplyContent .messageBody');
859 this._container
.hide();
861 // remove previous error messages
862 $messageBody
.children('small.innerError').remove();
866 $messageBody
.children('.icon-spinner').remove();
867 $messageBody
.children('#cke_text').show();
869 // display form submit
870 $messageBody
.next().show();
874 * Prepares jump to extended message add form.
876 _prepareExtended: function() {
877 this._pendingSave
= true;
879 // mark quotes for removal
880 if (this._quoteManager
!== null) {
881 this._quoteManager
.markQuotesForRemoval();
886 if ($.browser
.ckeditor
) {
887 var $ckEditor
= this._messageField
.ckeditorGet();
888 $message
= $ckEditor
.getData();
891 $message
= this._messageField
.val();
894 new WCF
.Action
.Proxy({
897 actionName
: 'jumpToExtended',
898 className
: this._getClassName(),
899 interfaceName
: 'wcf\\data\\IExtendedMessageQuickReplyAction',
901 containerID
: this._getObjectID(),
905 success: function(data
, textStatus
, jqXHR
) {
906 window
.location
= data
.returnValues
.url
;
912 * Handles successful AJAX calls.
915 * @param string textStatus
916 * @param jQuery jqXHR
918 _success: function(data
, textStatus
, jqXHR
) {
919 // redirect to new page
920 if (data
.returnValues
.url
) {
921 window
.location
= data
.returnValues
.url
;
924 if (data
.returnValues
.template
) {
926 var $message
= $('' + data
.returnValues
.template
);
927 if (this._container
.data('sortOrder') == 'DESC') {
928 $message
.insertAfter(this._container
);
931 $message
.insertBefore(this._container
);
934 // update last post time
935 this._container
.data('lastPostTime', data
.returnValues
.lastPostTime
);
938 this._notification
.show(undefined, undefined, WCF
.Language
.get('wcf.global.success.add'));
940 this._updateHistory($message
.wcfIdentify());
944 var $message
= (this._successMessageNonVisible
) ? this._successMessageNonVisible
: 'wcf.global.success.add';
945 this._notification
.show(undefined, 5000, WCF
.Language
.get($message
));
948 if ($.browser
.ckeditor
) {
949 // remove CKEditor contents
950 this._messageField
.ckeditorGet().setData('');
953 this._messageField
.val('');
956 // hide quick reply and revert it
957 this._revertQuickReply(true);
959 // count stored quotes
960 if (this._quoteManager
!== null) {
961 this._quoteManager
.countQuotes();
964 this._pendingSave
= false;
969 * Reverts quick reply on failure to preserve entered message.
971 _failure: function(data
) {
972 this._pendingSave
= false;
973 this._revertQuickReply(false);
975 if (data
=== null || data
.returnValues
=== undefined || data
.returnValues
.errorType
=== undefined) {
979 var $messageBody
= this._container
.find('.messageQuickReplyContent .messageBody');
980 var $innerError
= $messageBody
.children('small.innerError').empty();
981 if (!$innerError
.length
) {
982 $innerError
= $('<small class="innerError" />').appendTo($messageBody
);
985 $innerError
.html(data
.returnValues
.errorType
);
991 * Returns action class name.
995 _getClassName: function() {
1000 * Returns object id.
1004 _getObjectID: function() {
1009 * Updates the history to avoid old content when going back in the browser
1014 _updateHistory: function(hash
) {
1015 window
.location
.hash
= hash
;
1020 * Provides an inline message editor.
1022 * @param integer containerID
1024 WCF
.Message
.InlineEditor
= Class
.extend({
1026 * currently active message
1029 _activeElementID
: '',
1056 * CSS selector for the message container
1059 _messageContainerSelector
: '.jsMessage',
1062 * prefix of the message editor CSS id
1065 _messageEditorIDPrefix
: 'messageEditor',
1068 * notification object
1069 * @var WCF.System.Notification
1071 _notification
: null,
1075 * @var WCF.Action.Proxy
1080 * quote manager object
1081 * @var WCF.Message.Quote.Manager
1083 _quoteManager
: null,
1086 * support for extended editing form
1089 _supportExtendedForm
: false,
1092 * Initializes a new WCF.Message.InlineEditor object.
1094 * @param integer containerID
1095 * @param boolean supportExtendedForm
1096 * @param WCF.Message.Quote.Manager quoteManager
1098 init: function(containerID
, supportExtendedForm
, quoteManager
) {
1099 this._activeElementID
= '';
1101 this._container
= { };
1102 this._containerID
= parseInt(containerID
);
1103 this._dropdowns
= { };
1104 this._quoteManager
= quoteManager
|| null;
1105 this._supportExtendedForm
= (supportExtendedForm
) ? true : false;
1106 this._proxy
= new WCF
.Action
.Proxy({
1107 failure
: $.proxy(this._failure
, this),
1108 showLoadingOverlay
: false,
1109 success
: $.proxy(this._success
, this)
1111 this._notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.global.success.edit'));
1113 this.initContainers();
1115 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.InlineEditor', $.proxy(this.initContainers
, this));
1119 * Initializes editing capability for all messages.
1121 initContainers: function() {
1122 $(this._messageContainerSelector
).each($.proxy(function(index
, container
) {
1123 var $container
= $(container
);
1124 var $containerID
= $container
.wcfIdentify();
1126 if (!this._container
[$containerID
]) {
1127 this._container
[$containerID
] = $container
;
1129 if ($container
.data('canEditInline')) {
1130 var $button
= $container
.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID
).click($.proxy(this._clickInline
, this));
1131 if ($container
.data('canEdit')) $button
.dblclick($.proxy(this._click
, this));
1133 else if ($container
.data('canEdit')) {
1134 $container
.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID
).click($.proxy(this._click
, this));
1141 * Loads WYSIWYG editor for selected message.
1143 * @param object event
1144 * @param integer containerID
1147 _click: function(event
, containerID
) {
1148 var $containerID
= (event
=== null) ? containerID
: $(event
.currentTarget
).data('containerID');
1149 if (this._activeElementID
=== '') {
1150 this._activeElementID
= $containerID
;
1153 this._proxy
.setOption('data', {
1154 actionName
: 'beginEdit',
1155 className
: this._getClassName(),
1156 interfaceName
: 'wcf\\data\\IMessageInlineEditorAction',
1158 containerID
: this._containerID
,
1159 objectID
: this._container
[$containerID
].data('objectID')
1162 this._proxy
.setOption('failure', $.proxy(function() { this._cancel(); }, this));
1163 this._proxy
.sendRequest();
1166 var $notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.message.error.editorAlreadyInUse'), 'warning');
1167 $notification
.show();
1170 // force closing dropdown to avoid displaying the dropdown after
1172 if (this._dropdowns
[this._container
[$containerID
].data('objectID')]) {
1173 this._dropdowns
[this._container
[$containerID
].data('objectID')].removeClass('dropdownOpen');
1176 if (event
!== null) {
1177 event
.stopPropagation();
1183 * Provides an inline dropdown menu instead of directly loading the WYSIWYG editor.
1185 * @param object event
1188 _clickInline: function(event
) {
1189 var $button
= $(event
.currentTarget
);
1191 if (!$button
.hasClass('dropdownToggle')) {
1192 var $containerID
= $button
.data('containerID');
1194 $button
.addClass('dropdownToggle').parent().addClass('dropdown');
1196 var $dropdownMenu
= $('<ul class="dropdownMenu" />').insertAfter($button
);
1197 this._initDropdownMenu($containerID
, $dropdownMenu
);
1199 WCF
.DOMNodeInsertedHandler
.execute();
1201 this._dropdowns
[this._container
[$containerID
].data('objectID')] = $dropdownMenu
;
1203 WCF
.Dropdown
.registerCallback($button
.parent().wcfIdentify(), $.proxy(this._toggleDropdown
, this));
1205 // trigger click event
1206 $button
.trigger('click');
1209 event
.stopPropagation();
1214 * Handles errorneus editing requests.
1216 * @param object data
1218 _failure: function(data
) {
1219 this._revertEditor();
1221 if (data
=== null || data
.returnValues
=== undefined || data
.returnValues
.errorType
=== undefined) {
1225 var $messageBody
= this._container
[this._activeElementID
].find('.messageBody .messageInlineEditor');
1226 var $innerError
= $messageBody
.children('small.innerError').empty();
1227 if (!$innerError
.length
) {
1228 $innerError
= $('<small class="innerError" />').insertBefore($messageBody
.children('.formSubmit'));
1231 $innerError
.html(data
.returnValues
.errorType
);
1237 * Forces message options to stay visible if toggling dropdown menu.
1239 * @param string containerID
1240 * @param string action
1242 _toggleDropdown: function(containerID
, action
) {
1243 WCF
.Dropdown
.getDropdown(containerID
).parents('.messageOptions').toggleClass('forceOpen');
1247 * Initializes the inline edit dropdown menu.
1249 * @param integer containerID
1250 * @param jQuery dropdownMenu
1252 _initDropdownMenu: function(containerID
, dropdownMenu
) { },
1255 * Prepares message for WYSIWYG display.
1257 _prepare: function() {
1258 var $messageBody
= this._container
[this._activeElementID
].find('.messageBody');
1259 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody
);
1261 var $content
= $messageBody
.find('.messageText');
1263 // hide unrelated content
1264 $content
.parent().children('.jsInlineEditorHideContent').hide();
1266 this._cache
= $content
.detach();
1270 * Cancels editing and reverts to original message.
1272 _cancel: function() {
1273 var $container
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget');
1277 var $ckEditor
= $('#' + this._messageEditorIDPrefix
+ $container
.data('objectID')).ckeditorGet();
1278 $ckEditor
.destroy();
1281 // CKEditor might be not initialized yet, ignore
1285 var $messageBody
= $container
.find('.messageBody');
1286 $messageBody
.children('.icon-spinner').remove();
1287 $messageBody
.children('div:eq(0)').html(this._cache
);
1289 // show unrelated content
1290 $messageBody
.find('.jsInlineEditorHideContent').show();
1292 // revert message options
1293 this._container
[this._activeElementID
].find('.messageOptions').removeClass('forceHidden');
1295 this._activeElementID
= '';
1297 if (this._quoteManager
) {
1298 this._quoteManager
.clearAlternativeCKEditor();
1303 * Handles successful AJAX calls.
1305 * @param object data
1306 * @param string textStatus
1307 * @param jQuery jqXHR
1309 _success: function(data
, textStatus
, jqXHR
) {
1310 switch (data
.returnValues
.actionName
) {
1312 this._showEditor(data
);
1316 this._showMessage(data
);
1322 * Shows WYSIWYG editor for active message.
1324 * @param object data
1326 _showEditor: function(data
) {
1327 // revert failure function
1328 this._proxy
.setOption('failure', $.proxy(this._failure
, this));
1330 var $messageBody
= this._container
[this._activeElementID
].addClass('jsInvalidQuoteTarget').find('.messageBody');
1331 $messageBody
.children('.icon-spinner').remove();
1332 var $content
= $messageBody
.children('div:eq(0)');
1335 $('' + data
.returnValues
.template
).appendTo($content
);
1338 var $formSubmit
= $content
.find('.formSubmit');
1339 var $saveButton
= $formSubmit
.find('button[data-type=save]').click($.proxy(this._save
, this));
1340 if (this._supportExtendedForm
) $formSubmit
.find('button[data-type=extended]').click($.proxy(this._prepareExtended
, this));
1341 $formSubmit
.find('button[data-type=cancel]').click($.proxy(this._cancel
, this));
1343 WCF
.Message
.Submit
.registerButton(
1344 this._messageEditorIDPrefix
+ this._container
[this._activeElementID
].data('objectID'),
1348 // hide message options
1349 this._container
[this._activeElementID
].find('.messageOptions').addClass('forceHidden');
1351 if ($.browser
.ckeditor
) {
1352 new WCF
.PeriodicalExecuter($.proxy(function(pe
) {
1355 var $ckEditor
= $('#' + this._messageEditorIDPrefix
+ this._container
[this._activeElementID
].data('objectID'));
1356 $ckEditor
.ckeditor(function() { this.ui
.editor
.focus(); });
1358 if (this._quoteManager
) {
1359 this._quoteManager
.setAlternativeCKEditor($ckEditor
);
1364 $('#' + this._messageEditorIDPrefix
+ this._container
[this._activeElementID
].data('objectID')).focus();
1371 _revertEditor: function() {
1372 var $messageBody
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget').find('.messageBody');
1373 $messageBody
.children('span.icon-spinner').remove();
1374 $messageBody
.children('div:eq(0)').children().show();
1376 // show unrelated content
1377 $messageBody
.find('.jsInlineEditorHideContent').show();
1379 if (this._quoteManager
) {
1380 this._quoteManager
.clearAlternativeCKEditor();
1385 * Saves editor contents.
1388 var $container
= this._container
[this._activeElementID
];
1389 var $objectID
= $container
.data('objectID');
1392 if ($.browser
.ckeditor
) {
1393 var $ckEditor
= $('#' + this._messageEditorIDPrefix
+ $objectID
).ckeditorGet();
1394 $message
= $ckEditor
.getData();
1397 $message
= $('#' + this._messageEditorIDPrefix
+ $objectID
).val();
1400 this._proxy
.setOption('data', {
1402 className
: this._getClassName(),
1403 interfaceName
: 'wcf\\data\\IMessageInlineEditorAction',
1405 containerID
: this._containerID
,
1412 this._proxy
.sendRequest();
1418 * Prepares jumping to extended editing mode.
1420 _prepareExtended: function() {
1421 var $container
= this._container
[this._activeElementID
];
1422 var $objectID
= $container
.data('objectID');
1425 if ($.browser
.ckeditor
) {
1426 var $ckEditor
= $('#' + this._messageEditorIDPrefix
+ $objectID
).ckeditorGet();
1427 $message
= $ckEditor
.getData();
1430 $message
= $('#' + this._messageEditorIDPrefix
+ $objectID
).val();
1433 new WCF
.Action
.Proxy({
1436 actionName
: 'jumpToExtended',
1437 className
: this._getClassName(),
1439 containerID
: this._containerID
,
1441 messageID
: $objectID
1444 success: function(data
, textStatus
, jqXHR
) {
1445 window
.location
= data
.returnValues
.url
;
1451 * Hides WYSIWYG editor.
1453 _hideEditor: function() {
1454 var $messageBody
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget').find('.messageBody');
1455 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody
);
1456 $messageBody
.children('div:eq(0)').children().hide();
1458 // show unrelated content
1459 $messageBody
.find('.jsInlineEditorHideContent').show();
1461 if (this._quoteManager
) {
1462 this._quoteManager
.clearAlternativeCKEditor();
1467 * Shows rendered message.
1469 * @param object data
1471 _showMessage: function(data
) {
1472 var $container
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget');
1473 var $messageBody
= $container
.find('.messageBody');
1474 $messageBody
.children('.icon-spinner').remove();
1475 var $content
= $messageBody
.children('div:eq(0)');
1477 // show unrelated content
1478 $content
.parent().children('.jsInlineEditorHideContent').show();
1480 // revert message options
1481 this._container
[this._activeElementID
].find('.messageOptions').removeClass('forceHidden');
1484 if ($.browser
.ckeditor
) {
1485 var $ckEditor
= $('#' + this._messageEditorIDPrefix
+ $container
.data('objectID')).ckeditorGet();
1486 $ckEditor
.destroy();
1491 // insert new message
1492 $content
.html('<div class="messageText">' + data
.returnValues
.message
+ '</div>');
1494 this._activeElementID
= '';
1496 this._updateHistory(this._getHash($container
.data('objectID')));
1498 this._notification
.show();
1500 if (this._quoteManager
) {
1501 this._quoteManager
.clearAlternativeCKEditor();
1506 * Returns message action class name.
1510 _getClassName: function() {
1515 * Returns the hash added to the url after successfully editing a message.
1519 _getHash: function(objectID
) {
1520 return '#message' + objectID
;
1524 * Updates the history to avoid old content when going back in the browser
1529 _updateHistory: function(hash
) {
1530 window
.location
.hash
= hash
;
1535 * Handles submit buttons for forms with an embedded WYSIWYG editor.
1537 WCF
.Message
.Submit
= {
1539 * list of registered buttons
1545 * Registers submit button for specified wysiwyg container id.
1547 * @param string wysiwygContainerID
1548 * @param string selector
1550 registerButton: function(wysiwygContainerID
, selector
) {
1551 if (!WCF
.Browser
.isChrome()) {
1555 this._buttons
[wysiwygContainerID
] = $(selector
);
1559 * Triggers 'click' event for registered buttons.
1561 execute: function(wysiwygContainerID
) {
1562 if (!this._buttons
[wysiwygContainerID
]) {
1566 this._buttons
[wysiwygContainerID
].trigger('click');
1571 * Namespace for message quotes.
1573 WCF
.Message
.Quote
= { };
1576 * Handles message quotes.
1578 * @param string className
1579 * @param string objectType
1580 * @param string containerSelector
1581 * @param string messageBodySelector
1583 WCF
.Message
.Quote
.Handler
= Class
.extend({
1585 * active container id
1588 _activeContainerID
: '',
1597 * list of message containers
1603 * container selector
1606 _containerSelector
: '',
1609 * 'copy quote' overlay
1621 * message body selector
1624 _messageBodySelector
: '',
1640 * @var WCF.Action.Proxy
1646 * @var WCF.Message.Quote.Manager
1648 _quoteManager
: null,
1651 * Initializes the quote handler for given object type.
1653 * @param WCF.Message.Quote.Manager quoteManager
1654 * @param string className
1655 * @param string objectType
1656 * @param string containerSelector
1657 * @param string messageBodySelector
1658 * @param string messageContentSelector
1660 init: function(quoteManager
, className
, objectType
, containerSelector
, messageBodySelector
, messageContentSelector
) {
1661 this._className
= className
;
1662 if (this._className
== '') {
1663 console
.debug("[WCF.Message.QuoteManager] Empty class name given, aborting.");
1667 this._objectType
= objectType
;
1668 if (this._objectType
== '') {
1669 console
.debug("[WCF.Message.QuoteManager] Empty object type name given, aborting.");
1673 this._containerSelector
= containerSelector
;
1675 this._messageBodySelector
= messageBodySelector
;
1676 this._messageContentSelector
= messageContentSelector
;
1678 this._proxy
= new WCF
.Action
.Proxy({
1679 success
: $.proxy(this._success
, this)
1682 this._initContainers();
1683 this._initCopyQuote();
1685 $(document
).mouseup($.proxy(this._mouseUp
, this));
1687 // register with quote manager
1688 this._quoteManager
= quoteManager
;
1689 this._quoteManager
.register(this._objectType
, this);
1691 // register with DOMNodeInsertedHandler
1692 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.Quote.Handler' + objectType
.hashCode(), $.proxy(this._initContainers
, this));
1696 * Initializes message containers.
1698 _initContainers: function() {
1700 $(this._containerSelector
).each(function(index
, container
) {
1701 var $container
= $(container
);
1702 var $containerID
= $container
.wcfIdentify();
1704 if (!self
._containers
[$containerID
]) {
1705 self
._containers
[$containerID
] = $container
;
1706 if ($container
.hasClass('jsInvalidQuoteTarget')) {
1710 if (self
._messageBodySelector
!== null) {
1711 $container
= $container
.find(self
._messageBodySelector
).data('containerID', $containerID
);
1714 $container
.mousedown($.proxy(self
._mouseDown
, self
));
1716 // bind event to quote whole message
1717 self
._containers
[$containerID
].find('.jsQuoteMessage').click($.proxy(self
._saveFullQuote
, self
));
1723 * Handles mouse down event.
1725 * @param object event
1727 _mouseDown: function(event
) {
1729 this._copyQuote
.hide();
1731 // store container ID
1732 var $container
= $(event
.currentTarget
);
1734 if (this._messageBodySelector
) {
1735 $container
= this._containers
[$container
.data('containerID')];
1738 if ($container
.hasClass('jsInvalidQuoteTarget')) {
1739 this._activeContainerID
= '';
1744 this._activeContainerID
= $container
.wcfIdentify();
1746 // remove alt-tag from all images, fixes quoting in Firefox
1747 if ($.browser
.mozilla
) {
1748 $container
.find('img').each(function() {
1749 var $image
= $(this);
1750 $image
.data('__alt', $image
.attr('alt')).removeAttr('alt');
1756 * Returns the text of a node and its children.
1758 * @param object node
1761 _getNodeText: function(node
) {
1764 for (var i
= 0; i
< node
.childNodes
.length
; i
++) {
1765 if (node
.childNodes
[i
].nodeType
== 3) {
1767 nodeText
+= node
.childNodes
[i
].nodeValue
;
1770 if (!node
.childNodes
[i
].tagName
) {
1774 var $tagName
= node
.childNodes
[i
].tagName
.toLowerCase();
1775 if ($tagName
=== 'li') {
1778 else if ($tagName
=== 'td' && !$.browser
.msie
) {
1782 nodeText
+= this._getNodeText(node
.childNodes
[i
]);
1784 if ($tagName
=== 'ul') {
1794 * Handles the mouse up event.
1796 * @param object event
1798 _mouseUp: function(event
) {
1800 if (this._activeContainerID
== '') {
1801 this._copyQuote
.hide();
1806 var $container
= this._containers
[this._activeContainerID
];
1807 var $selection
= this._getSelectedText();
1808 var $text
= $.trim($selection
);
1810 this._copyQuote
.hide();
1815 // compare selection with message text of given container
1816 var $messageText
= null;
1817 if (this._messageBodySelector
) {
1818 $messageText
= this._getNodeText($container
.find(this._messageContentSelector
).get(0));
1821 $messageText
= this._getNodeText($container
.get(0));
1824 // selected text is not part of $messageText or contains text from unrelated nodes
1825 if (this._normalize($messageText
).indexOf(this._normalize($text
)) === -1) {
1828 this._copyQuote
.show();
1830 var $coordinates
= this._getBoundingRectangle($container
, $selection
);
1831 var $dimensions
= this._copyQuote
.getDimensions('outer');
1832 var $left
= ($coordinates
.right
- $coordinates
.left
) / 2 - ($dimensions
.width
/ 2) + $coordinates
.left
;
1834 this._copyQuote
.css({
1835 top
: $coordinates
.top
- $dimensions
.height
- 7 + 'px',
1838 this._copyQuote
.hide();
1840 // reset containerID
1841 this._activeContainerID
= '';
1843 // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
1845 new WCF
.PeriodicalExecuter(function(pe
) {
1848 var $text
= $.trim(self
._getSelectedText());
1850 self
._copyQuote
.show();
1851 self
._message
= $text
;
1852 self
._objectID
= $container
.data('objectID');
1854 // revert alt tags, fixes quoting in Firefox
1855 if ($.browser
.mozilla
) {
1856 $container
.find('img').each(function() {
1857 var $image
= $(this);
1858 $image
.attr('alt', $image
.data('__alt'));
1866 * Normalizes a text for comparison.
1868 * @param string text
1871 _normalize: function(text
) {
1872 return text
.replace(/\r?\n|\r/g, "\n").replace(/\s/g, ' ').replace(/\s{2,}/g, ' ');
1876 * Returns the left or right offset of the current text selection.
1878 * @param object range
1879 * @param boolean before
1882 _getOffset: function(range
, before
) {
1883 range
.collapse(before
);
1885 var $elementID
= WCF
.getRandomID();
1886 var $element
= document
.createElement('span');
1887 $element
.innerHTML
= '<span id="' + $elementID
+ '"></span>';
1888 var $fragment
= document
.createDocumentFragment(), $node
;
1889 while ($node
= $element
.firstChild
) {
1890 $fragment
.appendChild($node
);
1892 range
.insertNode($fragment
);
1894 $element
= $('#' + $elementID
);
1895 var $position
= $element
.offset();
1896 $position
.top
= $position
.top
- $(window
).scrollTop();
1903 * Returns the offsets of the selection's bounding rectangle.
1907 _getBoundingRectangle: function(container
, selection
) {
1908 var $coordinates
= null;
1910 if (document
.createRange
&& typeof document
.createRange().getBoundingClientRect
!= "undefined") { // Opera, Firefox, Safari, Chrome
1911 if (selection
.rangeCount
> 0) {
1912 // the coordinates returned by getBoundingClientRect() is relative to the window, not the document!
1913 //var $rect = selection.getRangeAt(0).getBoundingClientRect();
1914 var $rects
= selection
.getRangeAt(0).getClientRects();
1915 var $rect
= selection
.getRangeAt(0).getBoundingClientRect();
1919 if (!$.browser.mozilla && $rects.length > 1) {
1920 // save current selection to restore it later
1921 var $range = selection.getRangeAt(0);
1922 var $bckp = this._saveSelection(container.get(0));
1923 var $position1 = this._getOffset($range, true);
1925 var $range = selection.getRangeAt(0);
1926 var $position2 = this._getOffset($range, false);
1929 left: Math.min($position1.left, $position2.left),
1930 right: Math.max($position1.left, $position2.left),
1931 top: Math.max($position1.top, $position2.top)
1934 // restore selection
1935 this._restoreSelection(container.get(0), $bckp);
1938 $rect = selection.getRangeAt(0).getBoundingClientRect();
1942 var $document
= $(document
);
1943 var $offsetTop
= $document
.scrollTop();
1948 top
: $rect
.top
+ $offsetTop
1952 else if (document
.selection
&& document
.selection
.type
!= "Control") { // IE
1953 var $range
= document
.selection
.createRange();
1956 left
: $range
.boundingLeft
,
1957 right
: $range
.boundingRight
,
1958 top
: $range
.boundingTop
1962 return $coordinates
;
1966 * Saves current selection.
1968 * @see http://stackoverflow.com/a/13950376
1970 * @param object containerEl
1973 _saveSelection: function(containerEl
) {
1974 if (window
.getSelection
&& document
.createRange
) {
1975 var range
= window
.getSelection().getRangeAt(0);
1976 var preSelectionRange
= range
.cloneRange();
1977 preSelectionRange
.selectNodeContents(containerEl
);
1978 preSelectionRange
.setEnd(range
.startContainer
, range
.startOffset
);
1979 var start
= preSelectionRange
.toString().length
;
1983 end
: start
+ range
.toString().length
1987 var selectedTextRange
= document
.selection
.createRange();
1988 var preSelectionTextRange
= document
.body
.createTextRange();
1989 preSelectionTextRange
.moveToElementText(containerEl
);
1990 preSelectionTextRange
.setEndPoint("EndToStart", selectedTextRange
);
1991 var start
= preSelectionTextRange
.text
.length
;
1995 end
: start
+ selectedTextRange
.text
.length
2001 * Restores a selection.
2003 * @see http://stackoverflow.com/a/13950376
2005 * @param object containerEl
2006 * @param object savedSel
2008 _restoreSelection: function(containerEl
, savedSel
) {
2009 if (window
.getSelection
&& document
.createRange
) {
2010 var charIndex
= 0, range
= document
.createRange();
2011 range
.setStart(containerEl
, 0);
2012 range
.collapse(true);
2013 var nodeStack
= [containerEl
], node
, foundStart
= false, stop
= false;
2015 while (!stop
&& (node
= nodeStack
.pop())) {
2016 if (node
.nodeType
== 3) {
2017 var nextCharIndex
= charIndex
+ node
.length
;
2018 if (!foundStart
&& savedSel
.start
>= charIndex
&& savedSel
.start
<= nextCharIndex
) {
2019 range
.setStart(node
, savedSel
.start
- charIndex
);
2022 if (foundStart
&& savedSel
.end
>= charIndex
&& savedSel
.end
<= nextCharIndex
) {
2023 range
.setEnd(node
, savedSel
.end
- charIndex
);
2026 charIndex
= nextCharIndex
;
2028 var i
= node
.childNodes
.length
;
2030 nodeStack
.push(node
.childNodes
[i
]);
2035 var sel
= window
.getSelection();
2036 sel
.removeAllRanges();
2037 sel
.addRange(range
);
2040 var textRange
= document
.body
.createTextRange();
2041 textRange
.moveToElementText(containerEl
);
2042 textRange
.collapse(true);
2043 textRange
.moveEnd("character", savedSel
.end
);
2044 textRange
.moveStart("character", savedSel
.start
);
2050 * Initializes the 'copy quote' element.
2052 _initCopyQuote: function() {
2053 this._copyQuote
= $('#quoteManagerCopy');
2054 if (!this._copyQuote
.length
) {
2055 this._copyQuote
= $('<div id="quoteManagerCopy" class="balloonTooltip"><span>' + WCF
.Language
.get('wcf.message.quote.quoteSelected') + '</span><span class="pointer"><span></span></span></div>').hide().appendTo(document
.body
);
2056 this._copyQuote
.click($.proxy(this._saveQuote
, this));
2061 * Returns the text selection.
2065 _getSelectedText: function() {
2066 if (window
.getSelection
) { // Opera, Firefox, Safari, Chrome, IE 9+
2067 return window
.getSelection();
2069 else if (document
.getSelection
) { // Opera, Firefox, Safari, Chrome, IE 9+
2070 return document
.getSelection();
2072 else if (document
.selection
) { // IE 8
2073 return document
.selection
.createRange().text
;
2080 * Saves a full quote.
2082 * @param object event
2084 _saveFullQuote: function(event
) {
2085 var $listItem
= $(event
.currentTarget
);
2087 this._proxy
.setOption('data', {
2088 actionName
: 'saveFullQuote',
2089 className
: this._className
,
2090 interfaceName
: 'wcf\\data\\IMessageQuoteAction',
2091 objectIDs
: [ $listItem
.data('objectID') ]
2093 this._proxy
.sendRequest();
2095 // mark element as quoted
2096 if ($listItem
.data('isQuoted')) {
2097 $listItem
.data('isQuoted', false).children('a').removeClass('active');
2100 $listItem
.data('isQuoted', true).children('a').addClass('active');
2104 event
.stopPropagation();
2111 _saveQuote: function() {
2112 this._proxy
.setOption('data', {
2113 actionName
: 'saveQuote',
2114 className
: this._className
,
2115 interfaceName
: 'wcf\\data\\IMessageQuoteAction',
2116 objectIDs
: [ this._objectID
],
2118 message
: this._message
2121 this._proxy
.sendRequest();
2125 * Handles successful AJAX requests.
2127 * @param object data
2128 * @param string textStatus
2129 * @param jQuery jqXHR
2131 _success: function(data
, textStatus
, jqXHR
) {
2132 if (data
.returnValues
.count
!== undefined) {
2133 var $fullQuoteObjectIDs
= (data
.fullQuoteObjectIDs
!== undefined) ? data
.fullQuoteObjectIDs
: { };
2134 this._quoteManager
.updateCount(data
.returnValues
.count
, $fullQuoteObjectIDs
);
2139 * Updates the full quote data for all matching objects.
2141 * @param array<integer> $objectIDs
2143 updateFullQuoteObjectIDs: function(objectIDs
) {
2144 for (var $containerID
in this._containers
) {
2145 this._containers
[$containerID
].find('.jsQuoteMessage').each(function(index
, button
) {
2146 // reset all markings
2147 var $button
= $(button
).data('isQuoted', 0);
2148 $button
.children('a').removeClass('active');
2151 if (WCF
.inArray($button
.data('objectID'), objectIDs
)) {
2152 $button
.data('isQuoted', 1).children('a').addClass('active');
2160 * Manages stored quotes.
2162 * @param integer count
2164 WCF
.Message
.Quote
.Manager
= Class
.extend({
2166 * list of form buttons
2178 * alternative CKEditor element
2181 _ckEditorAlternative
: null,
2184 * number of stored quotes
2202 * list of quote handlers
2208 * true, if an up-to-date template exists
2211 _hasTemplate
: false,
2214 * true, if related quotes should be inserted
2217 _insertQuotes
: true,
2221 * @var WCF.Action.Proxy
2226 * list of quotes to remove upon submit
2227 * @var array<string>
2229 _removeOnSubmit
: [ ],
2232 * show quotes element
2241 _supportPaste
: false,
2244 * Initializes the quote manager.
2246 * @param integer count
2247 * @param string ckEditorID
2248 * @param boolean supportPaste
2249 * @param array<string> removeOnSubmit
2251 init: function(count
, ckEditorID
, supportPaste
, removeOnSubmit
) {
2256 this._ckEditor
= null;
2257 this._ckEditorAlternative
= null;
2258 this._count
= parseInt(count
) || 0;
2259 this._dialog
= null;
2261 this._handlers
= { };
2262 this._hasTemplate
= false;
2263 this._insertQuotes
= true;
2264 this._removeOnSubmit
= [ ];
2265 this._showQuotes
= null;
2266 this._supportPaste
= false;
2269 this._ckEditor
= $('#' + ckEditorID
);
2270 if (this._ckEditor
.length
) {
2271 this._supportPaste
= true;
2273 // get surrounding form-tag
2274 this._form
= this._ckEditor
.parents('form:eq(0)');
2275 if (this._form
.length
) {
2276 this._form
.submit($.proxy(this._submit
, this));
2277 this._removeOnSubmit
= removeOnSubmit
|| [ ];
2283 this._supportPaste
= (supportPaste
=== true) ? true : false;
2288 this._proxy
= new WCF
.Action
.Proxy({
2289 showLoadingOverlay
: false,
2290 success
: $.proxy(this._success
, this),
2291 url
: 'index.php/MessageQuote/?t=' + SECURITY_TOKEN
+ SID_ARG_2ND
2294 this._toggleShowQuotes();
2298 * Sets an alternative CKEditor instance on runtime.
2300 * @param jQuery ckEditor
2302 setAlternativeCKEditor: function(ckEditor
) {
2303 this._ckEditorAlternative
= ckEditor
;
2307 * Clears alternative CKEditor instance.
2309 clearAlternativeCKEditor: function() {
2310 this._ckEditorAlternative
= null;
2314 * Registers a quote handler.
2316 * @param string objectType
2317 * @param WCF.Message.Quote.Handler handler
2319 register: function(objectType
, handler
) {
2320 this._handlers
[objectType
] = handler
;
2324 * Updates number of stored quotes.
2326 * @param integer count
2327 * @param object fullQuoteObjectIDs
2329 updateCount: function(count
, fullQuoteObjectIDs
) {
2330 this._count
= parseInt(count
) || 0;
2332 this._toggleShowQuotes();
2334 // update full quote ids of handlers
2335 for (var $objectType
in this._handlers
) {
2336 if (fullQuoteObjectIDs
[$objectType
]) {
2337 this._handlers
[$objectType
].updateFullQuoteObjectIDs(fullQuoteObjectIDs
[$objectType
]);
2343 * Inserts all associated quotes upon first time using quick reply.
2345 * @param string className
2346 * @param integer parentObjectID
2347 * @param object callback
2349 insertQuotes: function(className
, parentObjectID
, callback
) {
2350 if (!this._insertQuotes
) {
2351 this._insertQuotes
= true;
2356 new WCF
.Action
.Proxy({
2359 actionName
: 'getRenderedQuotes',
2360 className
: className
,
2361 interfaceName
: 'wcf\\data\\IMessageQuoteAction',
2363 parentObjectID
: parentObjectID
2371 * Toggles the display of the 'Show quotes' button
2373 _toggleShowQuotes: function() {
2375 if (this._showQuotes
!== null) {
2376 this._showQuotes
.hide();
2380 if (this._showQuotes
=== null) {
2381 this._showQuotes
= $('#showQuotes');
2382 if (!this._showQuotes
.length
) {
2383 this._showQuotes
= $('<div id="showQuotes" class="balloonTooltip" />').click($.proxy(this._click
, this)).appendTo(document
.body
);
2387 var $text
= WCF
.Language
.get('wcf.message.quote.showQuotes').replace(/#count#/, this._count
);
2388 this._showQuotes
.text($text
).show();
2391 this._hasTemplate
= false;
2395 * Handles clicks on 'Show quotes'.
2397 _click: function() {
2398 if (this._hasTemplate
) {
2399 this._dialog
.wcfDialog('open');
2402 this._proxy
.showLoadingOverlayOnce();
2404 this._proxy
.setOption('data', {
2405 actionName
: 'getQuotes',
2406 supportPaste
: this._supportPaste
2408 this._proxy
.sendRequest();
2413 * Renders the dialog.
2415 * @param string template
2417 renderDialog: function(template
) {
2418 // create dialog if not exists
2419 if (this._dialog
=== null) {
2420 this._dialog
= $('#messageQuoteList');
2421 if (!this._dialog
.length
) {
2422 this._dialog
= $('<div id="messageQuoteList" />').hide().appendTo(document
.body
);
2427 this._dialog
.html(template
);
2429 // add 'insert' and 'delete' buttons
2430 var $formSubmit
= $('<div class="formSubmit" />').appendTo(this._dialog
);
2431 if (this._supportPaste
) this._buttons
.insert
= $('<button class="buttonPrimary">' + WCF
.Language
.get('wcf.message.quote.insertAllQuotes') + '</button>').click($.proxy(this._insertSelected
, this)).appendTo($formSubmit
);
2432 this._buttons
.remove
= $('<button>' + WCF
.Language
.get('wcf.message.quote.removeAllQuotes') + '</button>').click($.proxy(this._removeSelected
, this)).appendTo($formSubmit
);
2435 this._dialog
.wcfDialog({
2436 title
: WCF
.Language
.get('wcf.message.quote.manageQuotes')
2438 this._dialog
.wcfDialog('render');
2439 this._hasTemplate
= true;
2441 // bind event listener
2442 var $insertQuoteButtons
= this._dialog
.find('.jsInsertQuote');
2443 if (this._supportPaste
) {
2444 $insertQuoteButtons
.click($.proxy(this._insertQuote
, this));
2447 $insertQuoteButtons
.hide();
2450 this._dialog
.find('input.jsCheckbox').change($.proxy(this._changeButtons
, this));
2452 // mark quotes for removal
2453 if (this._removeOnSubmit
.length
) {
2455 this._dialog
.find('input.jsRemoveQuote').each(function(index
, input
) {
2456 var $input
= $(input
).change($.proxy(this._change
, this));
2458 // mark for deletion
2459 if (WCF
.inArray($input
.parent('li').attr('data-quote-id'), self
._removeOnSubmit
)) {
2460 $input
.attr('checked', 'checked');
2467 * Updates button labels if a checkbox is checked or unchecked.
2469 _changeButtons: function() {
2471 if (this._dialog
.find('input.jsCheckbox:checked').length
) {
2472 if (this._supportPaste
) this._buttons
.insert
.html(WCF
.Language
.get('wcf.message.quote.insertSelectedQuotes'));
2473 this._buttons
.remove
.html(WCF
.Language
.get('wcf.message.quote.removeSelectedQuotes'));
2476 // no selection, pick all
2477 if (this._supportPaste
) this._buttons
.insert
.html(WCF
.Language
.get('wcf.message.quote.insertAllQuotes'));
2478 this._buttons
.remove
.html(WCF
.Language
.get('wcf.message.quote.removeAllQuotes'));
2483 * Checks for change event on delete-checkboxes.
2485 * @param object event
2487 _change: function(event
) {
2488 var $input
= $(event
.currentTarget
);
2489 var $quoteID
= $input
.parent('li').attr('data-quote-id');
2491 if ($input
.prop('checked')) {
2492 this._removeOnSubmit
.push($quoteID
);
2495 for (var $index
in this._removeOnSubmit
) {
2496 if (this._removeOnSubmit
[$index
] == $quoteID
) {
2497 delete this._removeOnSubmit
[$index
];
2505 * Inserts the selected quotes.
2507 _insertSelected: function() {
2508 if (this._ckEditorAlternative
=== null) {
2509 var $api
= $('.jsQuickReply:eq(0)').data('__api');
2510 if ($api
&& !$api
.getContainer().is(':visible')) {
2511 this._insertQuotes
= false;
2516 if (!this._dialog
.find('input.jsCheckbox:checked').length
) {
2517 this._dialog
.find('input.jsCheckbox').prop('checked', 'checked');
2520 // insert all quotes
2521 this._dialog
.find('input.jsCheckbox:checked').each($.proxy(function(index
, input
) {
2522 this._insertQuote(null, input
);
2526 this._dialog
.wcfDialog('close');
2532 * @param object event
2533 * @param object inputElement
2535 _insertQuote: function(event
, inputElement
) {
2536 if (event
!== null && this._ckEditorAlternative
=== null) {
2537 var $api
= $('.jsQuickReply:eq(0)').data('__api');
2538 if ($api
&& !$api
.getContainer().is(':visible')) {
2539 this._insertQuotes
= false;
2544 var $listItem
= (event
=== null) ? $(inputElement
).parents('li') : $(event
.currentTarget
).parents('li');
2545 var $quote
= $.trim($listItem
.children('div.jsFullQuote').text());
2546 var $message
= $listItem
.parents('article.message');
2549 $quote
= "[quote='" + $message
.attr('data-username') + "','" + $message
.data('link') + "']" + $quote
+ "[/quote]";
2551 // insert into ckEditor
2552 var $ckEditor
= null;
2553 if ($.browser
.ckeditor
) {
2554 if (this._ckEditorAlternative
=== null) {
2555 $ckEditor
= this._ckEditor
.ckeditorGet();
2558 $ckEditor
= this._ckEditorAlternative
.ckeditorGet();
2562 if ($ckEditor
!== null && $ckEditor
.mode
=== 'wysiwyg') {
2565 // remove the link if the cursor is in a link element
2566 $ckEditor
.removeStyle(new CKEDITOR
.style({
2568 type
: CKEDITOR
.STYLE_INLINE
2571 $ckEditor
.insertText($quote
+ "\n\n");
2575 var $textarea
= null;
2576 if (this._ckEditorAlternative
=== null) {
2577 $textarea
= ($.browser
.ckeditor
) ? this._ckEditor
.next('.cke_editor_text').find('textarea') : this._ckEditor
;
2580 $textarea
= ($.browser
.ckeditor
) ? this._ckEditorAlternative
.next('.cke_editor_text').find('textarea') : this._ckEditorAlternative
;
2583 var $value
= $textarea
.val();
2585 if ($value
.length
== 0) {
2586 $textarea
.val($quote
);
2589 var $position
= $textarea
.getCaret();
2590 $textarea
.val( $value
.substr(0, $position
) + $quote
+ $value
.substr($position
) );
2594 // remove quote upon submit or upon request
2595 this._removeOnSubmit
.push($listItem
.attr('data-quote-id'));
2598 if (event
!== null) {
2599 this._dialog
.wcfDialog('close');
2604 * Removes selected quotes.
2606 _removeSelected: function() {
2607 if (!this._dialog
.find('input.jsCheckbox:checked').length
) {
2608 this._dialog
.find('input.jsCheckbox').prop('checked', 'checked');
2611 var $quoteIDs
= [ ];
2612 this._dialog
.find('input.jsCheckbox:checked').each(function(index
, input
) {
2613 $quoteIDs
.push($(input
).parents('li').attr('data-quote-id'));
2616 if ($quoteIDs
.length
) {
2618 var $objectTypes
= [ ];
2619 for (var $objectType
in this._handlers
) {
2620 $objectTypes
.push($objectType
);
2623 this._proxy
.setOption('data', {
2624 actionName
: 'remove',
2625 getFullQuoteObjectIDs
: this._handlers
.length
> 0,
2626 objectTypes
: $objectTypes
,
2629 this._proxy
.sendRequest();
2631 this._dialog
.wcfDialog('close');
2636 * Appends list of quote ids to remove after successful submit.
2638 _submit: function() {
2639 if (this._supportPaste
&& this._removeOnSubmit
.length
> 0) {
2640 var $formSubmit
= this._form
.find('.formSubmit');
2641 for (var $i
in this._removeOnSubmit
) {
2642 $('<input type="hidden" name="__removeQuoteIDs[]" value="' + this._removeOnSubmit
[$i
] + '" />').appendTo($formSubmit
);
2648 * Returns a list of quote ids marked for removal.
2650 * @return array<integer>
2652 getQuotesMarkedForRemoval: function() {
2653 return this._removeOnSubmit
;
2657 * Marks quote ids for removal.
2659 markQuotesForRemoval: function() {
2660 if (this._removeOnSubmit
.length
) {
2661 this._proxy
.setOption('data', {
2662 actionName
: 'markForRemoval',
2663 quoteIDs
: this._removeOnSubmit
2665 this._proxy
.suppressErrors();
2666 this._proxy
.sendRequest();
2671 * Removes all marked quote ids.
2673 removeMarkedQuotes: function() {
2674 if (this._removeOnSubmit
.length
) {
2675 this._proxy
.setOption('data', {
2676 actionName
: 'removeMarkedQuotes',
2677 getFullQuoteObjectIDs
: this._handlers
.length
> 0
2679 this._proxy
.sendRequest();
2684 * Counts stored quotes.
2686 countQuotes: function() {
2687 var $objectTypes
= [ ];
2688 for (var $objectType
in this._handlers
) {
2689 $objectTypes
.push($objectType
);
2692 this._proxy
.setOption('data', {
2693 actionName
: 'count',
2694 getFullQuoteObjectIDs
: this._handlers
.length
> 0,
2695 objectTypes
: $objectTypes
2697 this._proxy
.sendRequest();
2701 * Handles successful AJAX requests.
2703 * @param object data
2704 * @param string textStatus
2705 * @param jQuery jqXHR
2707 _success: function(data
, textStatus
, jqXHR
) {
2708 if (data
=== null) {
2712 if (data
.count
!== undefined) {
2713 var $fullQuoteObjectIDs
= (data
.fullQuoteObjectIDs
!== undefined) ? data
.fullQuoteObjectIDs
: { };
2714 this.updateCount(data
.count
, $fullQuoteObjectIDs
);
2717 if (data
.template
!== undefined) {
2718 if ($.trim(data
.template
) == '') {
2719 this.updateCount(0, { });
2722 this.renderDialog(data
.template
);
2729 * Namespace for message sharing related classes.
2731 WCF
.Message
.Share
= { };
2734 * Displays a dialog overlay for permalinks.
2736 WCF
.Message
.Share
.Content
= Class
.extend({
2738 * list of cached templates
2750 * Initializes the WCF.Message.Share.Content class.
2754 this._dialog
= null;
2758 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.Share.Content', $.proxy(this._initLinks
, this));
2762 * Initializes share links.
2764 _initLinks: function() {
2765 $('a.jsButtonShare').removeClass('jsButtonShare').click($.proxy(this._click
, this));
2769 * Displays links to share this content.
2771 * @param object event
2773 _click: function(event
) {
2774 event
.preventDefault();
2776 var $target
= $(event
.currentTarget
);
2777 var $link
= $target
.prop('href');
2778 var $title
= ($target
.data('linkTitle') ? $target
.data('linkTitle') : $link
);
2779 var $key
= $link
.hashCode();
2780 if (this._cache
[$key
] === undefined) {
2781 // remove dialog contents
2782 var $dialogInitialized
= false;
2783 if (this._dialog
=== null) {
2784 this._dialog
= $('<div />').hide().appendTo(document
.body
);
2785 $dialogInitialized
= true;
2788 this._dialog
.empty();
2791 // permalink (plain text)
2792 var $fieldset
= $('<fieldset><legend><label for="__sharePermalink">' + WCF
.Language
.get('wcf.message.share.permalink') + '</label></legend></fieldset>').appendTo(this._dialog
);
2793 $('<input type="text" id="__sharePermalink" class="long" readonly="readonly" />').attr('value', $link
).appendTo($fieldset
);
2795 // permalink (BBCode)
2796 var $fieldset
= $('<fieldset><legend><label for="__sharePermalinkBBCode">' + WCF
.Language
.get('wcf.message.share.permalink.bbcode') + '</label></legend></fieldset>').appendTo(this._dialog
);
2797 $('<input type="text" id="__sharePermalinkBBCode" class="long" readonly="readonly" />').attr('value', '[url=\'' + $link
+ '\']' + $title
+ '[/url]').appendTo($fieldset
);
2800 var $fieldset
= $('<fieldset><legend><label for="__sharePermalinkHTML">' + WCF
.Language
.get('wcf.message.share.permalink.html') + '</label></legend></fieldset>').appendTo(this._dialog
);
2801 $('<input type="text" id="__sharePermalinkHTML" class="long" readonly="readonly" />').attr('value', '<a href="' + $link
+ '">' + WCF
.String
.escapeHTML($title
) + '</a>').appendTo($fieldset
);
2803 this._cache
[$key
] = this._dialog
.html();
2805 if ($dialogInitialized
) {
2806 this._dialog
.wcfDialog({
2807 title
: WCF
.Language
.get('wcf.message.share')
2811 this._dialog
.wcfDialog('open');
2815 this._dialog
.html(this._cache
[$key
]).wcfDialog('open');
2818 this._enableSelection();
2822 * Enables text selection.
2824 _enableSelection: function() {
2825 var $inputElements
= this._dialog
.find('input').click(function() { $(this).select(); });
2827 // Safari on iOS can only select the text if it is not readonly and setSelectionRange() is used
2828 if (navigator
.userAgent
.match(/iP(ad|hone|od)/)) {
2829 $inputElements
.keydown(function() { return false; }).removeAttr('readonly').click(function() { this.setSelectionRange(0, 9999); });
2835 * Provides buttons to share a page through multiple social community sites.
2837 * @param boolean fetchObjectCount
2839 WCF
.Message
.Share
.Page
= Class
.extend({
2841 * list of share buttons
2850 _pageDescription
: '',
2853 * canonical page URL
2859 * Initializes the WCF.Message.Share.Page class.
2861 * @param boolean fetchObjectCount
2863 init: function(fetchObjectCount
) {
2864 this._pageDescription
= encodeURIComponent($('meta[property="og:title"]').prop('content'));
2865 this._pageURL
= encodeURIComponent($('meta[property="og:url"]').prop('content'));
2867 var $container
= $('.messageShareButtons');
2869 facebook
: $container
.find('.jsShareFacebook'),
2870 google
: $container
.find('.jsShareGoogle'),
2871 reddit
: $container
.find('.jsShareReddit'),
2872 twitter
: $container
.find('.jsShareTwitter')
2875 this._ui
.facebook
.children('a').click($.proxy(this._shareFacebook
, this));
2876 this._ui
.google
.children('a').click($.proxy(this._shareGoogle
, this));
2877 this._ui
.reddit
.children('a').click($.proxy(this._shareReddit
, this));
2878 this._ui
.twitter
.children('a').click($.proxy(this._shareTwitter
, this));
2880 if (fetchObjectCount
=== true) {
2881 this._fetchFacebook();
2882 this._fetchTwitter();
2883 this._fetchReddit();
2888 * Shares current page to selected social community site.
2890 * @param string objectName
2892 * @param boolean appendURL
2894 _share: function(objectName
, url
, appendURL
) {
2895 window
.open(url
.replace(/{pageURL}/, this._pageURL
).replace(/{text}/, this._pageDescription
+ (appendURL
? " " + this._pageURL
: "")), 'height=600,width=600');
2899 * Shares current page with Facebook.
2901 _shareFacebook: function() {
2902 this._share('facebook', 'https://www.facebook.com/sharer.php?u={pageURL}&t={text}', true);
2906 * Shares current page with Google Plus.
2908 _shareGoogle: function() {
2909 this._share('google', 'https://plus.google.com/share?url={pageURL}', true);
2913 * Shares current page with Reddit.
2915 _shareReddit: function() {
2916 this._share('reddit', 'https://ssl.reddit.com/submit?url={pageURL}', true);
2920 * Shares current page with Twitter.
2922 _shareTwitter: function() {
2923 this._share('twitter', 'https://twitter.com/share?url={pageURL}&text={text}', false);
2927 * Fetches share count from a social community site.
2930 * @param object callback
2931 * @param string callbackName
2933 _fetchCount: function(url
, callback
, callbackName
) {
2937 showLoadingOverlay
: false,
2939 suppressErrors
: true,
2941 url
: url
.replace(/{pageURL}/, this._pageURL
)
2944 $options
.jsonp
= callbackName
;
2947 new WCF
.Action
.Proxy($options
);
2951 * Fetches number of Facebook likes.
2953 _fetchFacebook: function() {
2954 this._fetchCount('https://graph.facebook.com/?id={pageURL}', $.proxy(function(data
) {
2956 this._ui
.facebook
.children('span.badge').show().text(data
.shares
);
2962 * Fetches tweet count from Twitter.
2964 _fetchTwitter: function() {
2965 if (window
.location
.protocol
.match(/^https/)) return;
2967 this._fetchCount('http://urls.api.twitter.com/1/urls/count.json?url={pageURL}', $.proxy(function(data
) {
2969 this._ui
.twitter
.children('span.badge').show().text(data
.count
);
2975 * Fetches cumulative vote sum from Reddit.
2977 _fetchReddit: function() {
2978 if (window
.location
.protocol
.match(/^https/)) return;
2980 this._fetchCount('http://www.reddit.com/api/info.json?url={pageURL}', $.proxy(function(data
) {
2981 if (data
.data
.children
.length
) {
2982 this._ui
.reddit
.children('span.badge').show().text(data
.data
.children
[0].data
.score
);
2989 * Handles user mention suggestions in CKEditors.
2991 * Important: Objects of this class have to be created before the CKEditor
2994 WCF
.Message
.UserMention
= Class
.extend({
2996 * name of the class used to get the user suggestions
2999 _className
: 'wcf\\data\\user\\UserAction',
3008 * dropdown menu object
3011 _dropdownMenu
: null,
3014 * suggestion item index, -1 if none is selected
3026 * current beginning of the mentioning
3032 * Initalizes user suggestions for the CKEditor with the given textarea id.
3034 * @param string editorID
3036 init: function(editorID
) {
3037 // temporary disable suggestions for Internet Explorer
3039 // this issue is caused by misplacing the range within the parent element,
3040 // while the typed chars are appended to the preceeding text node, without
3041 // getting focused for some reason
3042 if ($.browser
.msie
) {
3046 this._textarea
= $('#' + editorID
);
3048 // get associated (ready) CKEditor object and add event listeners
3049 CKEDITOR
.on('instanceReady', $.proxy(function(event
) {
3050 if (event
.editor
.name
=== this._textarea
.wcfIdentify()) {
3051 this._ckEditor
= event
.editor
;
3052 this._ckEditor
.container
.on('keyup', $.proxy(this._keyup
, this));
3053 this._ckEditor
.container
.on('keydown', $.proxy(this._keydown
, this));
3054 this._ckEditor
.on('key', $.proxy(this._key
, this));
3056 this._dropdown
= $(this._ckEditor
.editable().$);
3057 this._dropdownMenu
= $('<ul class="dropdownMenu userSuggestionList" />').appendTo(this._textarea
.parent());
3058 WCF
.Dropdown
.initDropdownFragment(this._dropdown
, this._dropdownMenu
);
3062 this._proxy
= new WCF
.Action
.Proxy({
3063 success
: $.proxy(this._success
, this)
3068 * Clears the suggestion list.
3070 _clearList: function() {
3073 this._dropdownMenu
.empty();
3077 * Handles a click on a list item suggesting a username.
3079 * @param object event
3081 _click: function(event
) {
3082 this._setUsername($(event
.currentTarget
).data('username'));
3086 * Creates an item in the suggestion list with the given data.
3090 _createListItem: function(listItemData
) {
3091 var $listItem
= $('<li />').data('username', listItemData
.label
).click($.proxy(this._click
, this)).appendTo(this._dropdownMenu
);
3093 var $box16
= $('<div />').addClass('box16').appendTo($listItem
);
3094 $box16
.append($(listItemData
.icon
).addClass('framed'));
3095 $box16
.append($('<div />').append($('<span />').text(listItemData
.label
)));
3099 * Returns the offsets used to set the position of the user suggestion
3104 _getDropdownMenuPosition: function() {
3105 var $range
= this._ckEditor
.getSelection().getRanges()[0];
3106 var $orgRange
= $range
.clone();
3107 var $startOffset
= $range
.startOffset
;
3109 // move caret after the '@' sign
3110 $range
.setStart($range
.startContainer
, $startOffset
- this._mentionStart
.length
);
3111 $range
.collapse(true);
3114 // create span with random id and add it in front of the '@' sign
3115 var $element
= document
.createElement('span');
3116 $node
= new CKEDITOR
.dom
.node($element
);
3117 $range
.insertNode($node
);
3119 // get offsets of span
3120 $jElement
= $($element
);
3121 if ($.browser
.opera
) {
3122 // in opera, the span's height is 0 if it has no content
3123 $jElement
.text(' ');
3125 var $offsets
= $jElement
.offset();
3126 if (this._lineHeight
=== null) {
3127 this._lineHeight
= $jElement
.height();
3130 // merge text nodes before and after the temporary span element
3131 // to avoid split text nodes which were one node before inserting
3132 // the span element since split nodes can cause problems working
3133 // with ranges and then remove the merged text node
3134 if (!$.browser
.msie
|| $element
.previousSibling
&& $element
.nextSibling
) {
3135 $element
.previousSibling
.nodeValue
+= $element
.nextSibling
.nodeValue
;
3136 $($element
.nextSibling
).remove();
3139 // reset caret position to original position at the end and make
3140 // sure that the range is the same in all browsers
3141 $range
.setStart($orgRange
.startContainer
, $startOffset
);
3142 $range
.setEnd($orgRange
.startContainer
, $startOffset
);
3152 * Returns the parameters for the AJAX request.
3156 _getParameters: function() {
3159 includeUserGroups
: false,
3160 searchString
: this._mentionStart
3166 * Returns the relevant text in front of the caret in the current line.
3170 _getTextLineInFrontOfCaret: function() {
3171 var $range
= this._ckEditor
.getSelection().getRanges()[0];
3173 // if text is marked, user suggestions are disabled
3174 if (!$range
.collapsed
) {
3178 var $text
= $range
.startContainer
.getText().substr(0, $range
.startOffset
);
3180 // remove unicode zero width space and no-breaking space
3181 var $textBackup
= $text
;
3183 for (var $i
= 0; $i
< $textBackup
.length
; $i
++) {
3184 var $byte = $textBackup
.charCodeAt($i
).toString(16);
3185 if ($byte != '200b' && !/\s/.test($textBackup
[$i
])) {
3186 if ($textBackup
[$i
] === '@' && $i
&& /\s/.test($textBackup
[$i
- 1])) {
3190 $text
+= $textBackup
[$i
];
3201 * Hides the suggestion list.
3203 _hideList: function() {
3204 this._dropdown
.removeClass('dropdownOpen');
3205 this._dropdownMenu
.removeClass('dropdownOpen');
3207 this._itemIndex
= -1;
3211 * Handles the key event of the CKEditor to select user suggestion on enter.
3213 _key: function(event
) {
3214 if (this._ckEditor
.mode
!== 'wysiwyg') {
3218 if (this._dropdownMenu
.is(':visible')) {
3219 if (event
.data
.keyCode
=== 13) { // enter
3220 this._dropdownMenu
.children('li').eq(this._itemIndex
).trigger('click');
3228 * Handles the keydown event to check if the user starts mentioning someone.
3230 * @param object event
3232 _keydown: function(event
) {
3233 if (this._ckEditor
.mode
!== 'wysiwyg') {
3237 if (this._dropdownMenu
.is(':visible')) {
3238 switch (event
.data
.$.keyCode
) {
3239 case 38: // arrow up
3240 event
.data
.$.preventDefault();
3242 this._selectItem(this._itemIndex
- 1);
3245 case 40: // arrow down
3246 event
.data
.$.preventDefault();
3248 this._selectItem(this._itemIndex
+ 1);
3255 * Handles the keyup event to check if the user starts mentioning someone.
3257 * @param object event
3259 _keyup: function(event
) {
3260 if (this._ckEditor
.mode
!== 'wysiwyg') {
3264 // ignore enter key up event
3265 if (event
.data
.$.keyCode
=== 13) {
3269 // ignore event if suggestion list and user pressed enter, arrow up or arrow down
3270 if (this._dropdownMenu
.is(':visible') && event
.data
.$.keyCode
in { 13:1, 38:1, 40:1 }) {
3274 var $currentText
= this._getTextLineInFrontOfCaret();
3276 var $match
= $currentText
.match(/@([^,]{3,})$/);
3278 // if mentioning is at text begin or there's a whitespace character
3279 // before the '@', everything is fine
3280 if (!$match
.index
|| $currentText
[$match
.index
- 1].match(/\s/)) {
3281 this._mentionStart
= $match
[1];
3283 this._proxy
.setOption('data', {
3284 actionName
: 'getSearchResultList',
3285 className
: this._className
,
3286 interfaceName
: 'wcf\\data\\ISearchAction',
3287 parameters
: this._getParameters()
3289 this._proxy
.sendRequest();
3302 * Replaces the started mentioning with a chosen username.
3304 _setUsername: function(username
) {
3305 var $range
= this._ckEditor
.getSelection().getRanges()[0];
3307 // remove the beginning of the username and the '@'
3308 $range
.setStart($range
.startContainer
, $range
.startOffset
- (this._mentionStart
.length
+ 1));
3310 // if an existing mention is edited, remove the link around it
3311 var $removedLink
= false;
3312 var $commonAncestor
= $range
.getCommonAncestor();
3313 if ($commonAncestor
.getText() == '@' + this._mentionStart
) {
3314 if ($commonAncestor
.getParent() && $commonAncestor
.getParent().getName() == 'a') {
3315 $commonAncestor
.replace($commonAncestor
.getParent());
3316 $range
.setStart($commonAncestor
);
3319 $range
.deleteContents();
3322 if (username
.indexOf("'") !== -1) {
3323 username
= username
.replace(/'/g, "''");
3324 username = "'" + username + "'";
3326 else if (username.indexOf(' ') !== -1) {
3327 username = "'" + username + "'";
3329 var $usernameNode = new CKEDITOR.dom.text('@' + username);
3330 $range.insertNode($usernameNode);
3331 $range.selectNodeContents($usernameNode);
3332 $range.collapse(false);
3339 * Selects the suggestion with the given item index.
3341 * @param integer itemIndex
3343 _selectItem: function(itemIndex) {
3344 var $li = this._dropdownMenu.children('li
');
3346 if (itemIndex < 0) {
3347 itemIndex = $li.length - 1;
3349 else if (itemIndex + 1 > $li.length) {
3353 $li.removeClass('dropdownNavigationItem
');
3354 $li.eq(itemIndex).addClass('dropdownNavigationItem
');
3356 this._itemIndex = itemIndex;
3360 * Shows the suggestion list.
3362 _showList: function() {
3363 this._dropdown.addClass('dropdownOpen
');
3364 this._dropdownMenu.addClass('dropdownOpen
');
3368 * Evalutes user suggestion-AJAX request results.
3370 * @param object data
3371 * @param string textStatus
3372 * @param jQuery jqXHR
3374 _success: function(data, textStatus, jqXHR) {
3375 this._clearList(false);
3377 if ($.getLength(data.returnValues)) {
3378 for (var $i in data.returnValues) {
3379 var $item = data.returnValues[$i];
3380 this._createListItem($item);
3383 this._updateSuggestionListPosition();
3389 * Updates the position of the suggestion list.
3391 _updateSuggestionListPosition: function() {
3393 var $dropdownMenuPosition = this._getDropdownMenuPosition();
3394 $dropdownMenuPosition.top += 5 + this._lineHeight; // add little vertical gap
3395 $dropdownMenuPosition.left -= 16; // make sure dropdown arrow is at correct position
3396 this._dropdownMenu.css($dropdownMenuPosition);
3397 this._selectItem(0);
3399 if ($dropdownMenuPosition.top + this._dropdownMenu.outerHeight() + 10 > $(window).height() + $(document).scrollTop()) {
3400 this._dropdownMenu.addClass('dropdownArrowBottom
');
3402 this._dropdownMenu.css({
3403 top: $dropdownMenuPosition.top - this._dropdownMenu.outerHeight() - 2 * this._lineHeight + 5
3407 this._dropdownMenu.removeClass('dropdownArrowBottom
');
3411 // ignore errors that are caused by pressing enter to
3412 // often in a short period of time