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 Redactor 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 Redactor 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 Redactor or null if editor was not accessible.
232 _getMessage: function() {
233 if (!$.browser
.redactor
) {
234 return this._messageField
.val();
236 else if (this._messageField
.data('redactor')) {
237 return this._messageField
.redactor('getText');
244 * Handles successful AJAX requests.
247 * @param string textStatus
248 * @param jQuery jqXHR
250 _success: function(data
, textStatus
, jqXHR
) {
251 // restore preview button
252 this._previewButton
.html(this._previewButtonLabel
).enable();
254 // remove error message
255 this._messageField
.parent().children('small.innerError').remove();
258 this._handleResponse(data
);
262 * Evaluates response data.
266 _handleResponse: function(data
) { },
269 * Handles errors during preview requests.
271 * The return values indicates if the default error overlay is shown.
276 _failure: function(data
) {
277 if (data
=== null || data
.returnValues
=== undefined || data
.returnValues
.errorType
=== undefined) {
281 // restore preview button
282 this._previewButton
.html(this._previewButtonLabel
).enable();
284 var $innerError
= this._messageField
.next('small.innerError').empty();
285 if (!$innerError
.length
) {
286 $innerError
= $('<small class="innerError" />').appendTo(this._messageField
.parent());
289 $innerError
.html(data
.returnValues
.errorType
);
296 * Default implementation for message previews.
298 * @see WCF.Message.Preview
300 WCF
.Message
.DefaultPreview
= WCF
.Message
.Preview
.extend({
301 _attachmentObjectType
: null,
302 _attachmentObjectID
: null,
306 * @see WCF.Message.Preview.init()
308 init: function(attachmentObjectType
, attachmentObjectID
, tmpHash
) {
309 this._super('wcf\\data\\bbcode\\MessagePreviewAction', 'text', 'previewButton');
311 this._attachmentObjectType
= attachmentObjectType
|| null;
312 this._attachmentObjectID
= attachmentObjectID
|| null;
313 this._tmpHash
= tmpHash
|| null;
317 * @see WCF.Message.Preview._handleResponse()
319 _handleResponse: function(data
) {
320 var $preview
= $('#previewContainer');
321 if (!$preview
.length
) {
322 $preview
= $('<div class="container containerPadding marginTop" id="previewContainer"><fieldset><legend>' + WCF
.Language
.get('wcf.global.preview') + '</legend><div></div></fieldset>').prependTo($('#messageContainer')).wcfFadeIn();
325 $preview
.find('div:eq(0)').html(data
.returnValues
.message
);
327 new WCF
.Effect
.Scroll().scrollTo($preview
);
331 * @see WCF.Message.Preview._getParameters()
333 _getParameters: function(message
) {
334 var $parameters
= this._super(message
);
336 if (this._attachmentObjectType
!= null) {
337 $parameters
.attachmentObjectType
= this._attachmentObjectType
;
338 $parameters
.attachmentObjectID
= this._attachmentObjectID
;
339 $parameters
.tmpHash
= this._tmpHash
;
347 * Handles multilingualism for messages.
349 * @param integer languageID
350 * @param object availableLanguages
351 * @param boolean forceSelection
353 WCF
.Message
.Multilingualism
= Class
.extend({
355 * list of available languages
358 _availableLanguages
: { },
367 * language input element
370 _languageInput
: null,
373 * Initializes WCF.Message.Multilingualism
375 * @param integer languageID
376 * @param object availableLanguages
377 * @param boolean forceSelection
379 init: function(languageID
, availableLanguages
, forceSelection
) {
380 this._availableLanguages
= availableLanguages
;
381 this._languageID
= languageID
|| 0;
383 this._languageInput
= $('#languageID');
385 // preselect current language id
388 // register event listener
389 this._languageInput
.find('.dropdownMenu > li').click($.proxy(this._click
, this));
391 // add element to disable multilingualism
392 if (!forceSelection
) {
393 var $dropdownMenu
= this._languageInput
.find('.dropdownMenu');
394 $('<li class="dropdownDivider" />').appendTo($dropdownMenu
);
395 $('<li><span><span class="badge">' + this._availableLanguages
[0] + '</span></span></li>').click($.proxy(this._disable
, this)).appendTo($dropdownMenu
);
399 this._languageInput
.parents('form').submit($.proxy(this._submit
, this));
403 * Handles language selections.
405 * @param object event
407 _click: function(event
) {
408 this._languageID
= $(event
.currentTarget
).data('languageID');
413 * Disables language selection.
415 _disable: function() {
416 this._languageID
= 0;
421 * Updates selected language.
423 _updateLabel: function() {
424 this._languageInput
.find('.dropdownToggle > span').text(this._availableLanguages
[this._languageID
]);
428 * Sets language id upon submit.
430 _submit: function() {
431 this._languageInput
.next('input[name=languageID]').prop('value', this._languageID
);
436 * Loads smiley categories upon user request.
438 WCF
.Message
.SmileyCategories
= Class
.extend({
440 * list of already loaded category ids
441 * @var array<integer>
447 * @var WCF.Action.Proxy
452 * Initializes the smiley loader.
456 this._proxy
= new WCF
.Action
.Proxy({
457 success
: $.proxy(this._success
, this)
460 $('#smilies').on('wcftabsbeforeactivate', $.proxy(this._click
, this));
464 new WCF
.PeriodicalExecuter(function(pe
) {
467 self
._click({ }, { newTab
: $('#smilies > .menu li.ui-state-active') });
472 * Handles tab menu clicks.
474 * @param object event
477 _click: function(event
, ui
) {
478 var $categoryID
= parseInt($(ui
.newTab
).children('a').data('smileyCategoryID'));
480 if ($categoryID
&& !WCF
.inArray($categoryID
, this._cache
)) {
481 this._proxy
.setOption('data', {
482 actionName
: 'getSmilies',
483 className
: 'wcf\\data\\smiley\\category\\SmileyCategoryAction',
484 objectIDs
: [ $categoryID
]
486 this._proxy
.sendRequest();
491 * Handles successful AJAX requests.
494 * @param string textStatus
495 * @param jQuery jqXHR
497 _success: function(data
, textStatus
, jqXHR
) {
498 var $categoryID
= parseInt(data
.returnValues
.smileyCategoryID
);
499 this._cache
.push($categoryID
);
501 $('#smilies-' + $categoryID
).html(data
.returnValues
.template
);
506 * Handles smiley clicks.
508 WCF
.Message
.Smilies
= Class
.extend({
515 _wysiwygSelector
: '',
518 * Initializes the smiley handler.
520 * @param string wysiwygSelector
522 init: function(wysiwygSelector
) {
523 this._wysiwygSelector
= wysiwygSelector
;
525 WCF
.System
.Dependency
.Manager
.register('Redactor_' + this._wysiwygSelector
, $.proxy(function() {
526 this._redactor
= $('#' + this._wysiwygSelector
).redactor('getObject');
528 // add smiley click handler
529 $(document
).on('click', '.jsSmiley', $.proxy(this._smileyClick
, this));
534 * Handles tab smiley clicks.
536 * @param object event
538 _smileyClick: function(event
) {
539 var $target
= $(event
.currentTarget
);
540 var $smileyCode
= $target
.data('smileyCode');
541 var $smileyPath
= $target
.data('smileyPath');
544 this._redactor
.insertSmiley($smileyCode
, $smileyPath
, true);
549 * Provides an AJAX-based quick reply for messages.
551 WCF
.Message
.QuickReply
= Class
.extend({
553 * quick reply container
565 * notification object
566 * @var WCF.System.Notification
571 * true, if a request to save the message is pending
578 * @var WCF.Action.Proxy
583 * quote manager object
584 * @var WCF.Message.Quote.Manager
590 * @var WCF.Effect.Scroll
592 _scrollHandler
: null,
595 * success message for created but invisible messages
598 _successMessageNonVisible
: '',
601 * Initializes a new WCF.Message.QuickReply object.
603 * @param boolean supportExtendedForm
604 * @param WCF.Message.Quote.Manager quoteManager
606 init: function(supportExtendedForm
, quoteManager
) {
607 this._container
= $('#messageQuickReply');
608 this._container
.children('.message').addClass('jsInvalidQuoteTarget');
609 this._messageField
= $('#text');
610 this._pendingSave
= false;
611 if (!this._container
|| !this._messageField
) {
616 var $formSubmit
= this._container
.find('.formSubmit');
617 $formSubmit
.find('button[data-type=save]').click($.proxy(this._save
, this));
618 if (supportExtendedForm
) $formSubmit
.find('button[data-type=extended]').click($.proxy(this._prepareExtended
, this));
619 $formSubmit
.find('button[data-type=cancel]').click($.proxy(this._cancel
, this));
621 if (quoteManager
) this._quoteManager
= quoteManager
;
623 $('.jsQuickReply').data('__api', this).click($.proxy(this.click
, this));
625 this._proxy
= new WCF
.Action
.Proxy({
626 failure
: $.proxy(this._failure
, this),
627 showLoadingOverlay
: false,
628 success
: $.proxy(this._success
, this)
630 this._scroll
= new WCF
.Effect
.Scroll();
631 this._notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.global.success.add'));
632 this._successMessageNonVisible
= '';
636 * Handles clicks on reply button.
638 * @param object event
640 click: function(event
) {
641 this._container
.toggle();
643 if (this._container
.is(':visible')) {
644 // TODO: Scrolling is anything but smooth, better use the init callback
645 this._scroll
.scrollTo(this._container
, true);
647 WCF
.Message
.Submit
.registerButton('text', this._container
.find('.formSubmit button[data-type=save]'));
649 if (this._quoteManager
) {
650 // check if message field is empty
652 if ($.browser
.redactor
) {
653 if (this._messageField
.data('redactor')) {
654 $empty
= (!$.trim(this._messageField
.redactor('getText')));
655 this._editorCallback($empty
);
659 $empty
= (!this._messageField
.val().length
);
660 this._editorCallback($empty
);
666 if (event
!== null) {
667 event
.stopPropagation();
673 * Inserts quotes and focuses the editor.
675 _editorCallback: function(isEmpty
) {
677 this._quoteManager
.insertQuotes(this._getClassName(), this._getObjectID(), $.proxy(this._insertQuotes
, this));
680 if ($.browser
.redactor
) {
681 this._messageField
.redactor('focus');
684 this._messageField
.focus();
689 * Returns container element.
693 getContainer: function() {
694 return this._container
;
698 * Insertes quotes into the quick reply editor.
702 _insertQuotes: function(data
) {
703 if (!data
.returnValues
.template
) {
707 if ($.browser
.redactor
) {
708 this._messageField
.redactor('insertDynamic', data
.returnValues
.template
);
711 this._messageField
.val(data
.returnValues
.template
);
719 if (this._pendingSave
) {
724 if ($.browser
.redactor
) {
725 $message
= this._messageField
.redactor('getText');
728 $message
= $.trim(this._messageField
.val());
731 // check if message is empty
732 var $innerError
= this._messageField
.parent().find('small.innerError');
733 if ($message
=== '' || $message
=== '0') {
734 if (!$innerError
.length
) {
735 $innerError
= $('<small class="innerError" />').appendTo(this._messageField
.parent());
738 $innerError
.html(WCF
.Language
.get('wcf.global.form.error.empty'));
742 $innerError
.remove();
745 this._pendingSave
= true;
747 this._proxy
.setOption('data', {
748 actionName
: 'quickReply',
749 className
: this._getClassName(),
750 interfaceName
: 'wcf\\data\\IMessageQuickReplyAction',
751 parameters
: this._getParameters($message
)
753 this._proxy
.sendRequest();
755 // show spinner and hide Redactor
756 var $messageBody
= this._container
.find('.messageQuickReplyContent .messageBody');
757 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody
);
758 $messageBody
.children('.redactor_box').hide().end().next().hide();
762 * Returns the parameters for the save request.
764 * @param string message
767 _getParameters: function(message
) {
769 objectID
: this._getObjectID(),
773 lastPostTime
: this._container
.data('lastPostTime'),
774 pageNo
: this._container
.data('pageNo'),
775 removeQuoteIDs
: (this._quoteManager
=== null ? [ ] : this._quoteManager
.getQuotesMarkedForRemoval()),
776 tmpHash
: this._container
.data('tmpHash') || ''
778 if (this._container
.data('anchor')) {
779 $parameters
.anchor
= this._container
.data('anchor');
786 * Cancels quick reply.
788 _cancel: function() {
789 this._revertQuickReply(true);
791 if ($.browser
.redactor
) {
792 this._messageField
.redactor('reset');
795 this._messageField
.val('');
800 * Reverts quick reply to original state and optionally hiding it.
802 * @param boolean hide
804 _revertQuickReply: function(hide
) {
805 var $messageBody
= this._container
.find('.messageQuickReplyContent .messageBody');
808 this._container
.hide();
810 // remove previous error messages
811 $messageBody
.children('small.innerError').remove();
815 $messageBody
.children('.icon-spinner').remove();
816 $messageBody
.children('.redactor_box').show();
818 // display form submit
819 $messageBody
.next().show();
823 * Prepares jump to extended message add form.
825 _prepareExtended: function() {
826 this._pendingSave
= true;
828 // mark quotes for removal
829 if (this._quoteManager
!== null) {
830 this._quoteManager
.markQuotesForRemoval();
834 if ($.browser
.redactor
) {
835 $message
= this._messageField
.redactor('getText');
838 $message
= this._messageField
.val();
841 new WCF
.Action
.Proxy({
844 actionName
: 'jumpToExtended',
845 className
: this._getClassName(),
846 interfaceName
: 'wcf\\data\\IExtendedMessageQuickReplyAction',
848 containerID
: this._getObjectID(),
852 success: function(data
, textStatus
, jqXHR
) {
853 window
.location
= data
.returnValues
.url
;
859 * Handles successful AJAX calls.
862 * @param string textStatus
863 * @param jQuery jqXHR
865 _success: function(data
, textStatus
, jqXHR
) {
866 if ($.browser
.redactor
) {
867 this._messageField
.redactor('autosavePurge');
870 // redirect to new page
871 if (data
.returnValues
.url
) {
872 window
.location
= data
.returnValues
.url
;
875 if (data
.returnValues
.template
) {
877 var $message
= $('' + data
.returnValues
.template
);
878 if (this._container
.data('sortOrder') == 'DESC') {
879 $message
.insertAfter(this._container
);
882 $message
.insertBefore(this._container
);
885 // update last post time
886 this._container
.data('lastPostTime', data
.returnValues
.lastPostTime
);
889 this._notification
.show(undefined, undefined, WCF
.Language
.get('wcf.global.success.add'));
891 this._updateHistory($message
.wcfIdentify());
895 var $message
= (this._successMessageNonVisible
) ? this._successMessageNonVisible
: 'wcf.global.success.add';
896 this._notification
.show(undefined, 5000, WCF
.Language
.get($message
));
899 if ($.browser
.redactor
) {
900 this._messageField
.redactor('reset');
903 this._messageField
.val('');
906 // hide quick reply and revert it
907 this._revertQuickReply(true);
909 // count stored quotes
910 if (this._quoteManager
!== null) {
911 this._quoteManager
.countQuotes();
914 this._pendingSave
= false;
919 * Reverts quick reply on failure to preserve entered message.
921 _failure: function(data
) {
922 this._pendingSave
= false;
923 this._revertQuickReply(false);
925 if (data
=== null || data
.returnValues
=== undefined || data
.returnValues
.errorType
=== undefined) {
929 var $messageBody
= this._container
.find('.messageQuickReplyContent .messageBody');
930 var $innerError
= $messageBody
.children('small.innerError').empty();
931 if (!$innerError
.length
) {
932 $innerError
= $('<small class="innerError" />').appendTo($messageBody
);
935 $innerError
.html(data
.returnValues
.errorType
);
941 * Returns action class name.
945 _getClassName: function() {
954 _getObjectID: function() {
959 * Updates the history to avoid old content when going back in the browser
964 _updateHistory: function(hash
) {
965 window
.location
.hash
= hash
;
970 * Provides an inline message editor.
972 * @param integer containerID
974 WCF
.Message
.InlineEditor
= Class
.extend({
976 * currently active message
979 _activeElementID
: '',
1006 * CSS selector for the message container
1009 _messageContainerSelector
: '.jsMessage',
1012 * prefix of the message editor CSS id
1015 _messageEditorIDPrefix
: 'messageEditor',
1018 * notification object
1019 * @var WCF.System.Notification
1021 _notification
: null,
1025 * @var WCF.Action.Proxy
1030 * quote manager object
1031 * @var WCF.Message.Quote.Manager
1033 _quoteManager
: null,
1036 * support for extended editing form
1039 _supportExtendedForm
: false,
1042 * Initializes a new WCF.Message.InlineEditor object.
1044 * @param integer containerID
1045 * @param boolean supportExtendedForm
1046 * @param WCF.Message.Quote.Manager quoteManager
1048 init: function(containerID
, supportExtendedForm
, quoteManager
) {
1049 this._activeElementID
= '';
1051 this._container
= { };
1052 this._containerID
= parseInt(containerID
);
1053 this._dropdowns
= { };
1054 this._quoteManager
= quoteManager
|| null;
1055 this._supportExtendedForm
= (supportExtendedForm
) ? true : false;
1056 this._proxy
= new WCF
.Action
.Proxy({
1057 failure
: $.proxy(this._failure
, this),
1058 showLoadingOverlay
: false,
1059 success
: $.proxy(this._success
, this)
1061 this._notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.global.success.edit'));
1063 this.initContainers();
1065 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.InlineEditor', $.proxy(this.initContainers
, this));
1069 * Initializes editing capability for all messages.
1071 initContainers: function() {
1072 $(this._messageContainerSelector
).each($.proxy(function(index
, container
) {
1073 var $container
= $(container
);
1074 var $containerID
= $container
.wcfIdentify();
1076 if (!this._container
[$containerID
]) {
1077 this._container
[$containerID
] = $container
;
1079 if ($container
.data('canEditInline')) {
1080 var $button
= $container
.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID
).click($.proxy(this._clickInline
, this));
1081 if ($container
.data('canEdit')) $button
.dblclick($.proxy(this._click
, this));
1083 else if ($container
.data('canEdit')) {
1084 $container
.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID
).click($.proxy(this._click
, this));
1091 * Loads WYSIWYG editor for selected message.
1093 * @param object event
1094 * @param integer containerID
1097 _click: function(event
, containerID
) {
1098 var $containerID
= (event
=== null) ? containerID
: $(event
.currentTarget
).data('containerID');
1099 if (this._activeElementID
=== '') {
1100 this._activeElementID
= $containerID
;
1103 this._proxy
.setOption('data', {
1104 actionName
: 'beginEdit',
1105 className
: this._getClassName(),
1106 interfaceName
: 'wcf\\data\\IMessageInlineEditorAction',
1108 containerID
: this._containerID
,
1109 objectID
: this._container
[$containerID
].data('objectID')
1112 this._proxy
.setOption('failure', $.proxy(function() { this._cancel(); }, this));
1113 this._proxy
.sendRequest();
1116 var $notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.message.error.editorAlreadyInUse'), 'warning');
1117 $notification
.show();
1120 // force closing dropdown to avoid displaying the dropdown after
1122 if (this._dropdowns
[this._container
[$containerID
].data('objectID')]) {
1123 this._dropdowns
[this._container
[$containerID
].data('objectID')].removeClass('dropdownOpen');
1126 if (event
!== null) {
1127 event
.stopPropagation();
1133 * Provides an inline dropdown menu instead of directly loading the WYSIWYG editor.
1135 * @param object event
1138 _clickInline: function(event
) {
1139 var $button
= $(event
.currentTarget
);
1141 if (!$button
.hasClass('dropdownToggle')) {
1142 var $containerID
= $button
.data('containerID');
1144 $button
.addClass('dropdownToggle').parent().addClass('dropdown');
1146 var $dropdownMenu
= $('<ul class="dropdownMenu" />').insertAfter($button
);
1147 this._initDropdownMenu($containerID
, $dropdownMenu
);
1149 WCF
.DOMNodeInsertedHandler
.execute();
1151 this._dropdowns
[this._container
[$containerID
].data('objectID')] = $dropdownMenu
;
1153 WCF
.Dropdown
.registerCallback($button
.parent().wcfIdentify(), $.proxy(this._toggleDropdown
, this));
1155 // trigger click event
1156 $button
.trigger('click');
1159 event
.stopPropagation();
1164 * Handles errorneus editing requests.
1166 * @param object data
1168 _failure: function(data
) {
1169 this._revertEditor();
1171 if (data
=== null || data
.returnValues
=== undefined || data
.returnValues
.errorType
=== undefined) {
1175 var $messageBody
= this._container
[this._activeElementID
].find('.messageBody .messageInlineEditor');
1176 var $innerError
= $messageBody
.children('small.innerError').empty();
1177 if (!$innerError
.length
) {
1178 $innerError
= $('<small class="innerError" />').insertBefore($messageBody
.children('.formSubmit'));
1181 $innerError
.html(data
.returnValues
.errorType
);
1187 * Forces message options to stay visible if toggling dropdown menu.
1189 * @param string containerID
1190 * @param string action
1192 _toggleDropdown: function(containerID
, action
) {
1193 WCF
.Dropdown
.getDropdown(containerID
).parents('.messageOptions').toggleClass('forceOpen');
1197 * Initializes the inline edit dropdown menu.
1199 * @param integer containerID
1200 * @param jQuery dropdownMenu
1202 _initDropdownMenu: function(containerID
, dropdownMenu
) { },
1205 * Prepares message for WYSIWYG display.
1207 _prepare: function() {
1208 var $messageBody
= this._container
[this._activeElementID
].find('.messageBody');
1209 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody
);
1211 var $content
= $messageBody
.find('.messageText');
1213 // hide unrelated content
1214 $content
.parent().children('.jsInlineEditorHideContent').hide();
1215 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').hide();
1217 this._cache
= $content
.detach();
1221 * Cancels editing and reverts to original message.
1223 _cancel: function() {
1224 var $container
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget');
1227 var $target
= $('#' + this._messageEditorIDPrefix
+ $container
.data('objectID'));
1228 $target
.redactor('autosavePurge');
1229 $target
.redactor('destroy');
1232 var $messageBody
= $container
.find('.messageBody');
1233 $messageBody
.children('.icon-spinner').remove();
1234 $messageBody
.children('div:eq(0)').html(this._cache
);
1235 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').show();
1237 // show unrelated content
1238 $messageBody
.find('.jsInlineEditorHideContent').show();
1240 // revert message options
1241 this._container
[this._activeElementID
].find('.messageOptions').removeClass('forceHidden');
1243 this._activeElementID
= '';
1245 if (this._quoteManager
) {
1246 this._quoteManager
.clearAlternativeEditor();
1251 * Handles successful AJAX calls.
1253 * @param object data
1254 * @param string textStatus
1255 * @param jQuery jqXHR
1257 _success: function(data
, textStatus
, jqXHR
) {
1258 switch (data
.returnValues
.actionName
) {
1260 this._showEditor(data
);
1264 this._showMessage(data
);
1270 * Shows WYSIWYG editor for active message.
1272 * @param object data
1274 _showEditor: function(data
) {
1275 // revert failure function
1276 this._proxy
.setOption('failure', $.proxy(this._failure
, this));
1278 var $messageBody
= this._container
[this._activeElementID
].addClass('jsInvalidQuoteTarget').find('.messageBody');
1279 $messageBody
.children('.icon-spinner').remove();
1280 var $content
= $messageBody
.children('div:eq(0)');
1283 $('' + data
.returnValues
.template
).appendTo($content
);
1286 var $formSubmit
= $content
.find('.formSubmit');
1287 var $saveButton
= $formSubmit
.find('button[data-type=save]').click($.proxy(this._save
, this));
1288 if (this._supportExtendedForm
) $formSubmit
.find('button[data-type=extended]').click($.proxy(this._prepareExtended
, this));
1289 $formSubmit
.find('button[data-type=cancel]').click($.proxy(this._cancel
, this));
1291 WCF
.Message
.Submit
.registerButton(
1292 this._messageEditorIDPrefix
+ this._container
[this._activeElementID
].data('objectID'),
1296 // hide message options
1297 this._container
[this._activeElementID
].find('.messageOptions').addClass('forceHidden');
1299 var $element
= $('#' + this._messageEditorIDPrefix
+ this._container
[this._activeElementID
].data('objectID'));
1300 if ($.browser
.redactor
) {
1301 new WCF
.PeriodicalExecuter($.proxy(function(pe
) {
1304 if (this._quoteManager
) {
1305 this._quoteManager
.setAlternativeEditor($element
);
1317 _revertEditor: function() {
1318 var $messageBody
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget').find('.messageBody');
1319 $messageBody
.children('span.icon-spinner').remove();
1320 $messageBody
.children('div:eq(0)').children().show();
1321 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').show();
1323 // show unrelated content
1324 $messageBody
.find('.jsInlineEditorHideContent').show();
1326 if (this._quoteManager
) {
1327 this._quoteManager
.clearAlternativeEditor();
1332 * Saves editor contents.
1335 var $container
= this._container
[this._activeElementID
];
1336 var $objectID
= $container
.data('objectID');
1339 if ($.browser
.redactor
) {
1340 $message
= $('#' + this._messageEditorIDPrefix
+ $objectID
).redactor('getText');
1343 $message
= $('#' + this._messageEditorIDPrefix
+ $objectID
).val();
1346 this._proxy
.setOption('data', {
1348 className
: this._getClassName(),
1349 interfaceName
: 'wcf\\data\\IMessageInlineEditorAction',
1351 containerID
: this._containerID
,
1358 this._proxy
.sendRequest();
1364 * Prepares jumping to extended editing mode.
1366 _prepareExtended: function() {
1367 var $container
= this._container
[this._activeElementID
];
1368 var $objectID
= $container
.data('objectID');
1371 if ($.browser
.redactor
) {
1372 $message
= $('#' + this._messageEditorIDPrefix
+ $objectID
).redactor('getText');
1375 $message
= $('#' + this._messageEditorIDPrefix
+ $objectID
).val();
1378 new WCF
.Action
.Proxy({
1381 actionName
: 'jumpToExtended',
1382 className
: this._getClassName(),
1384 containerID
: this._containerID
,
1386 messageID
: $objectID
1389 success: function(data
, textStatus
, jqXHR
) {
1390 window
.location
= data
.returnValues
.url
;
1396 * Hides WYSIWYG editor.
1398 _hideEditor: function() {
1399 var $messageBody
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget').find('.messageBody');
1400 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody
);
1401 $messageBody
.children('div:eq(0)').children().hide();
1402 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').show();
1404 // show unrelated content
1405 $messageBody
.find('.jsInlineEditorHideContent').show();
1407 if (this._quoteManager
) {
1408 this._quoteManager
.clearAlternativeEditor();
1413 * Shows rendered message.
1415 * @param object data
1417 _showMessage: function(data
) {
1418 var $container
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget');
1419 var $messageBody
= $container
.find('.messageBody');
1420 $messageBody
.children('.icon-spinner').remove();
1421 var $content
= $messageBody
.children('div:eq(0)');
1423 // show unrelated content
1424 $content
.parent().children('.jsInlineEditorHideContent').show();
1426 // revert message options
1427 this._container
[this._activeElementID
].find('.messageOptions').removeClass('forceHidden');
1430 if ($.browser
.redactor
) {
1431 $('#' + this._messageEditorIDPrefix
+ $container
.data('objectID')).redactor('destroy');
1436 // insert new message
1437 $content
.html('<div class="messageText">' + data
.returnValues
.message
+ '</div>');
1439 if (data
.returnValues
.attachmentList
== undefined) {
1440 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').show();
1443 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').remove();
1445 if (data
.returnValues
.attachmentList
) {
1446 $(data
.returnValues
.attachmentList
).insertAfter($messageBody
.children('div:eq(0)'));
1450 this._activeElementID
= '';
1452 this._updateHistory(this._getHash($container
.data('objectID')));
1454 this._notification
.show();
1456 if (this._quoteManager
) {
1457 this._quoteManager
.clearAlternativeEditor();
1462 * Returns message action class name.
1466 _getClassName: function() {
1471 * Returns the hash added to the url after successfully editing a message.
1475 _getHash: function(objectID
) {
1476 return '#message' + objectID
;
1480 * Updates the history to avoid old content when going back in the browser
1485 _updateHistory: function(hash
) {
1486 window
.location
.hash
= hash
;
1491 * Handles submit buttons for forms with an embedded WYSIWYG editor.
1493 WCF
.Message
.Submit
= {
1495 * list of registered buttons
1501 * Registers submit button for specified wysiwyg container id.
1503 * @param string wysiwygContainerID
1504 * @param string selector
1506 registerButton: function(wysiwygContainerID
, selector
) {
1507 if (!WCF
.Browser
.isChrome()) {
1511 this._buttons
[wysiwygContainerID
] = $(selector
);
1515 * Triggers 'click' event for registered buttons.
1517 execute: function(wysiwygContainerID
) {
1518 if (!this._buttons
[wysiwygContainerID
]) {
1522 this._buttons
[wysiwygContainerID
].trigger('click');
1527 * Namespace for message quotes.
1529 WCF
.Message
.Quote
= { };
1532 * Handles message quotes.
1534 * @param string className
1535 * @param string objectType
1536 * @param string containerSelector
1537 * @param string messageBodySelector
1539 WCF
.Message
.Quote
.Handler
= Class
.extend({
1541 * active container id
1544 _activeContainerID
: '',
1553 * list of message containers
1559 * container selector
1562 _containerSelector
: '',
1565 * 'copy quote' overlay
1577 * message body selector
1580 _messageBodySelector
: '',
1596 * @var WCF.Action.Proxy
1602 * @var WCF.Message.Quote.Manager
1604 _quoteManager
: null,
1607 * Initializes the quote handler for given object type.
1609 * @param WCF.Message.Quote.Manager quoteManager
1610 * @param string className
1611 * @param string objectType
1612 * @param string containerSelector
1613 * @param string messageBodySelector
1614 * @param string messageContentSelector
1616 init: function(quoteManager
, className
, objectType
, containerSelector
, messageBodySelector
, messageContentSelector
) {
1617 this._className
= className
;
1618 if (this._className
== '') {
1619 console
.debug("[WCF.Message.QuoteManager] Empty class name given, aborting.");
1623 this._objectType
= objectType
;
1624 if (this._objectType
== '') {
1625 console
.debug("[WCF.Message.QuoteManager] Empty object type name given, aborting.");
1629 this._containerSelector
= containerSelector
;
1631 this._messageBodySelector
= messageBodySelector
;
1632 this._messageContentSelector
= messageContentSelector
;
1634 this._proxy
= new WCF
.Action
.Proxy({
1635 success
: $.proxy(this._success
, this)
1638 this._initContainers();
1639 this._initCopyQuote();
1641 $(document
).mouseup($.proxy(this._mouseUp
, this));
1643 // register with quote manager
1644 this._quoteManager
= quoteManager
;
1645 this._quoteManager
.register(this._objectType
, this);
1647 // register with DOMNodeInsertedHandler
1648 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.Quote.Handler' + objectType
.hashCode(), $.proxy(this._initContainers
, this));
1652 * Initializes message containers.
1654 _initContainers: function() {
1656 $(this._containerSelector
).each(function(index
, container
) {
1657 var $container
= $(container
);
1658 var $containerID
= $container
.wcfIdentify();
1660 if (!self
._containers
[$containerID
]) {
1661 self
._containers
[$containerID
] = $container
;
1662 if ($container
.hasClass('jsInvalidQuoteTarget')) {
1666 if (self
._messageBodySelector
!== null) {
1667 $container
= $container
.find(self
._messageBodySelector
).data('containerID', $containerID
);
1670 $container
.mousedown($.proxy(self
._mouseDown
, self
));
1672 // bind event to quote whole message
1673 self
._containers
[$containerID
].find('.jsQuoteMessage').click($.proxy(self
._saveFullQuote
, self
));
1679 * Handles mouse down event.
1681 * @param object event
1683 _mouseDown: function(event
) {
1685 this._copyQuote
.hide();
1687 // store container ID
1688 var $container
= $(event
.currentTarget
);
1690 if (this._messageBodySelector
) {
1691 $container
= this._containers
[$container
.data('containerID')];
1694 if ($container
.hasClass('jsInvalidQuoteTarget')) {
1695 this._activeContainerID
= '';
1700 this._activeContainerID
= $container
.wcfIdentify();
1702 // remove alt-tag from all images, fixes quoting in Firefox
1703 if ($.browser
.mozilla
) {
1704 $container
.find('img').each(function() {
1705 var $image
= $(this);
1706 $image
.data('__alt', $image
.attr('alt')).removeAttr('alt');
1712 * Returns the text of a node and its children.
1714 * @param object node
1717 _getNodeText: function(node
) {
1720 for (var i
= 0; i
< node
.childNodes
.length
; i
++) {
1721 if (node
.childNodes
[i
].nodeType
== 3) {
1723 nodeText
+= node
.childNodes
[i
].nodeValue
;
1726 if (!node
.childNodes
[i
].tagName
) {
1730 var $tagName
= node
.childNodes
[i
].tagName
.toLowerCase();
1731 if ($tagName
=== 'li') {
1734 else if ($tagName
=== 'td' && !$.browser
.msie
) {
1738 nodeText
+= this._getNodeText(node
.childNodes
[i
]);
1740 if ($tagName
=== 'ul') {
1750 * Handles the mouse up event.
1752 * @param object event
1754 _mouseUp: function(event
) {
1756 if (this._activeContainerID
== '') {
1757 this._copyQuote
.hide();
1762 var $container
= this._containers
[this._activeContainerID
];
1763 var $selection
= this._getSelectedText();
1764 var $text
= $.trim($selection
);
1766 this._copyQuote
.hide();
1771 // compare selection with message text of given container
1772 var $messageText
= null;
1773 if (this._messageBodySelector
) {
1774 $messageText
= this._getNodeText($container
.find(this._messageContentSelector
).get(0));
1777 $messageText
= this._getNodeText($container
.get(0));
1780 // selected text is not part of $messageText or contains text from unrelated nodes
1781 if (this._normalize($messageText
).indexOf(this._normalize($text
)) === -1) {
1784 this._copyQuote
.show();
1786 var $coordinates
= this._getBoundingRectangle($container
, $selection
);
1787 var $dimensions
= this._copyQuote
.getDimensions('outer');
1788 var $left
= ($coordinates
.right
- $coordinates
.left
) / 2 - ($dimensions
.width
/ 2) + $coordinates
.left
;
1790 this._copyQuote
.css({
1791 top
: $coordinates
.top
- $dimensions
.height
- 7 + 'px',
1794 this._copyQuote
.hide();
1796 // reset containerID
1797 this._activeContainerID
= '';
1799 // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
1801 new WCF
.PeriodicalExecuter(function(pe
) {
1804 var $text
= $.trim(self
._getSelectedText());
1806 self
._copyQuote
.show();
1807 self
._message
= $text
;
1808 self
._objectID
= $container
.data('objectID');
1810 // revert alt tags, fixes quoting in Firefox
1811 if ($.browser
.mozilla
) {
1812 $container
.find('img').each(function() {
1813 var $image
= $(this);
1814 $image
.attr('alt', $image
.data('__alt'));
1822 * Normalizes a text for comparison.
1824 * @param string text
1827 _normalize: function(text
) {
1828 return text
.replace(/\r?\n|\r/g, "\n").replace(/\s/g, ' ').replace(/\s{2,}/g, ' ');
1832 * Returns the left or right offset of the current text selection.
1834 * @param object range
1835 * @param boolean before
1838 _getOffset: function(range
, before
) {
1839 range
.collapse(before
);
1841 var $elementID
= WCF
.getRandomID();
1842 var $element
= document
.createElement('span');
1843 $element
.innerHTML
= '<span id="' + $elementID
+ '"></span>';
1844 var $fragment
= document
.createDocumentFragment(), $node
;
1845 while ($node
= $element
.firstChild
) {
1846 $fragment
.appendChild($node
);
1848 range
.insertNode($fragment
);
1850 $element
= $('#' + $elementID
);
1851 var $position
= $element
.offset();
1852 $position
.top
= $position
.top
- $(window
).scrollTop();
1859 * Returns the offsets of the selection's bounding rectangle.
1863 _getBoundingRectangle: function(container
, selection
) {
1864 var $coordinates
= null;
1866 if (document
.createRange
&& typeof document
.createRange().getBoundingClientRect
!= "undefined") { // Opera, Firefox, Safari, Chrome
1867 if (selection
.rangeCount
> 0) {
1868 // the coordinates returned by getBoundingClientRect() is relative to the window, not the document!
1869 //var $rect = selection.getRangeAt(0).getBoundingClientRect();
1870 var $rects
= selection
.getRangeAt(0).getClientRects();
1871 var $rect
= selection
.getRangeAt(0).getBoundingClientRect();
1875 if (!$.browser.mozilla && $rects.length > 1) {
1876 // save current selection to restore it later
1877 var $range = selection.getRangeAt(0);
1878 var $bckp = this._saveSelection(container.get(0));
1879 var $position1 = this._getOffset($range, true);
1881 var $range = selection.getRangeAt(0);
1882 var $position2 = this._getOffset($range, false);
1885 left: Math.min($position1.left, $position2.left),
1886 right: Math.max($position1.left, $position2.left),
1887 top: Math.max($position1.top, $position2.top)
1890 // restore selection
1891 this._restoreSelection(container.get(0), $bckp);
1894 $rect = selection.getRangeAt(0).getBoundingClientRect();
1898 var $document
= $(document
);
1899 var $offsetTop
= $document
.scrollTop();
1904 top
: $rect
.top
+ $offsetTop
1908 else if (document
.selection
&& document
.selection
.type
!= "Control") { // IE
1909 var $range
= document
.selection
.createRange();
1912 left
: $range
.boundingLeft
,
1913 right
: $range
.boundingRight
,
1914 top
: $range
.boundingTop
1918 return $coordinates
;
1922 * Saves current selection.
1924 * @see http://stackoverflow.com/a/13950376
1926 * @param object containerEl
1929 _saveSelection: function(containerEl
) {
1930 if (window
.getSelection
&& document
.createRange
) {
1931 var range
= window
.getSelection().getRangeAt(0);
1932 var preSelectionRange
= range
.cloneRange();
1933 preSelectionRange
.selectNodeContents(containerEl
);
1934 preSelectionRange
.setEnd(range
.startContainer
, range
.startOffset
);
1935 var start
= preSelectionRange
.toString().length
;
1939 end
: start
+ range
.toString().length
1943 var selectedTextRange
= document
.selection
.createRange();
1944 var preSelectionTextRange
= document
.body
.createTextRange();
1945 preSelectionTextRange
.moveToElementText(containerEl
);
1946 preSelectionTextRange
.setEndPoint("EndToStart", selectedTextRange
);
1947 var start
= preSelectionTextRange
.text
.length
;
1951 end
: start
+ selectedTextRange
.text
.length
1957 * Restores a selection.
1959 * @see http://stackoverflow.com/a/13950376
1961 * @param object containerEl
1962 * @param object savedSel
1964 _restoreSelection: function(containerEl
, savedSel
) {
1965 if (window
.getSelection
&& document
.createRange
) {
1966 var charIndex
= 0, range
= document
.createRange();
1967 range
.setStart(containerEl
, 0);
1968 range
.collapse(true);
1969 var nodeStack
= [containerEl
], node
, foundStart
= false, stop
= false;
1971 while (!stop
&& (node
= nodeStack
.pop())) {
1972 if (node
.nodeType
== 3) {
1973 var nextCharIndex
= charIndex
+ node
.length
;
1974 if (!foundStart
&& savedSel
.start
>= charIndex
&& savedSel
.start
<= nextCharIndex
) {
1975 range
.setStart(node
, savedSel
.start
- charIndex
);
1978 if (foundStart
&& savedSel
.end
>= charIndex
&& savedSel
.end
<= nextCharIndex
) {
1979 range
.setEnd(node
, savedSel
.end
- charIndex
);
1982 charIndex
= nextCharIndex
;
1984 var i
= node
.childNodes
.length
;
1986 nodeStack
.push(node
.childNodes
[i
]);
1991 var sel
= window
.getSelection();
1992 sel
.removeAllRanges();
1993 sel
.addRange(range
);
1996 var textRange
= document
.body
.createTextRange();
1997 textRange
.moveToElementText(containerEl
);
1998 textRange
.collapse(true);
1999 textRange
.moveEnd("character", savedSel
.end
);
2000 textRange
.moveStart("character", savedSel
.start
);
2006 * Initializes the 'copy quote' element.
2008 _initCopyQuote: function() {
2009 this._copyQuote
= $('#quoteManagerCopy');
2010 if (!this._copyQuote
.length
) {
2011 this._copyQuote
= $('<div id="quoteManagerCopy" class="balloonTooltip"><span>' + WCF
.Language
.get('wcf.message.quote.quoteSelected') + '</span><span class="pointer"><span></span></span></div>').hide().appendTo(document
.body
);
2012 this._copyQuote
.click($.proxy(this._saveQuote
, this));
2017 * Returns the text selection.
2021 _getSelectedText: function() {
2022 if (window
.getSelection
) { // Opera, Firefox, Safari, Chrome, IE 9+
2023 return window
.getSelection();
2025 else if (document
.getSelection
) { // Opera, Firefox, Safari, Chrome, IE 9+
2026 return document
.getSelection();
2028 else if (document
.selection
) { // IE 8
2029 return document
.selection
.createRange().text
;
2036 * Saves a full quote.
2038 * @param object event
2040 _saveFullQuote: function(event
) {
2041 var $listItem
= $(event
.currentTarget
);
2043 this._proxy
.setOption('data', {
2044 actionName
: 'saveFullQuote',
2045 className
: this._className
,
2046 interfaceName
: 'wcf\\data\\IMessageQuoteAction',
2047 objectIDs
: [ $listItem
.data('objectID') ]
2049 this._proxy
.sendRequest();
2051 // mark element as quoted
2052 if ($listItem
.data('isQuoted')) {
2053 $listItem
.data('isQuoted', false).children('a').removeClass('active');
2056 $listItem
.data('isQuoted', true).children('a').addClass('active');
2060 event
.stopPropagation();
2067 _saveQuote: function() {
2068 this._proxy
.setOption('data', {
2069 actionName
: 'saveQuote',
2070 className
: this._className
,
2071 interfaceName
: 'wcf\\data\\IMessageQuoteAction',
2072 objectIDs
: [ this._objectID
],
2074 message
: this._message
2077 this._proxy
.sendRequest();
2081 * Handles successful AJAX requests.
2083 * @param object data
2084 * @param string textStatus
2085 * @param jQuery jqXHR
2087 _success: function(data
, textStatus
, jqXHR
) {
2088 if (data
.returnValues
.count
!== undefined) {
2089 var $fullQuoteObjectIDs
= (data
.fullQuoteObjectIDs
!== undefined) ? data
.fullQuoteObjectIDs
: { };
2090 this._quoteManager
.updateCount(data
.returnValues
.count
, $fullQuoteObjectIDs
);
2095 * Updates the full quote data for all matching objects.
2097 * @param array<integer> $objectIDs
2099 updateFullQuoteObjectIDs: function(objectIDs
) {
2100 for (var $containerID
in this._containers
) {
2101 this._containers
[$containerID
].find('.jsQuoteMessage').each(function(index
, button
) {
2102 // reset all markings
2103 var $button
= $(button
).data('isQuoted', 0);
2104 $button
.children('a').removeClass('active');
2107 if (WCF
.inArray($button
.data('objectID'), objectIDs
)) {
2108 $button
.data('isQuoted', 1).children('a').addClass('active');
2116 * Manages stored quotes.
2118 * @param integer count
2120 WCF
.Message
.Quote
.Manager
= Class
.extend({
2122 * list of form buttons
2128 * number of stored quotes
2143 _editorElement
: null,
2146 * alternative Redactor element
2149 _editorElementAlternative
: null,
2158 * list of quote handlers
2164 * true, if an up-to-date template exists
2167 _hasTemplate
: false,
2170 * true, if related quotes should be inserted
2173 _insertQuotes
: true,
2177 * @var WCF.Action.Proxy
2182 * list of quotes to remove upon submit
2183 * @var array<string>
2185 _removeOnSubmit
: [ ],
2188 * show quotes element
2197 _supportPaste
: false,
2200 * Initializes the quote manager.
2202 * @param integer count
2203 * @param string elementID
2204 * @param boolean supportPaste
2205 * @param array<string> removeOnSubmit
2207 init: function(count
, elementID
, supportPaste
, removeOnSubmit
) {
2212 this._count
= parseInt(count
) || 0;
2213 this._dialog
= null;
2214 this._editorElement
= null;
2215 this._editorElementAlternative
= null;
2217 this._handlers
= { };
2218 this._hasTemplate
= false;
2219 this._insertQuotes
= true;
2220 this._removeOnSubmit
= [ ];
2221 this._showQuotes
= null;
2222 this._supportPaste
= false;
2225 this._editorElement
= $('#' + elementID
);
2226 if (this._editorElement
.length
) {
2227 this._supportPaste
= true;
2229 // get surrounding form-tag
2230 this._form
= this._editorElement
.parents('form:eq(0)');
2231 if (this._form
.length
) {
2232 this._form
.submit($.proxy(this._submit
, this));
2233 this._removeOnSubmit
= removeOnSubmit
|| [ ];
2239 this._supportPaste
= (supportPaste
=== true) ? true : false;
2244 this._proxy
= new WCF
.Action
.Proxy({
2245 showLoadingOverlay
: false,
2246 success
: $.proxy(this._success
, this),
2247 url
: 'index.php/MessageQuote/?t=' + SECURITY_TOKEN
+ SID_ARG_2ND
2250 this._toggleShowQuotes();
2254 * Sets an alternative editor element on runtime.
2256 * @param jQuery element
2258 setAlternativeEditor: function(element
) {
2259 this._editorElementAlternative
= element
;
2263 * Clears alternative editor element.
2265 clearAlternativeEditor: function() {
2266 this._editorElementAlternative
= null;
2270 * Registers a quote handler.
2272 * @param string objectType
2273 * @param WCF.Message.Quote.Handler handler
2275 register: function(objectType
, handler
) {
2276 this._handlers
[objectType
] = handler
;
2280 * Updates number of stored quotes.
2282 * @param integer count
2283 * @param object fullQuoteObjectIDs
2285 updateCount: function(count
, fullQuoteObjectIDs
) {
2286 this._count
= parseInt(count
) || 0;
2288 this._toggleShowQuotes();
2290 // update full quote ids of handlers
2291 for (var $objectType
in this._handlers
) {
2292 if (fullQuoteObjectIDs
[$objectType
]) {
2293 this._handlers
[$objectType
].updateFullQuoteObjectIDs(fullQuoteObjectIDs
[$objectType
]);
2299 * Inserts all associated quotes upon first time using quick reply.
2301 * @param string className
2302 * @param integer parentObjectID
2303 * @param object callback
2305 insertQuotes: function(className
, parentObjectID
, callback
) {
2306 if (!this._insertQuotes
) {
2307 this._insertQuotes
= true;
2312 new WCF
.Action
.Proxy({
2315 actionName
: 'getRenderedQuotes',
2316 className
: className
,
2317 interfaceName
: 'wcf\\data\\IMessageQuoteAction',
2319 parentObjectID
: parentObjectID
2327 * Toggles the display of the 'Show quotes' button
2329 _toggleShowQuotes: function() {
2331 if (this._showQuotes
!== null) {
2332 this._showQuotes
.hide();
2336 if (this._showQuotes
=== null) {
2337 this._showQuotes
= $('#showQuotes');
2338 if (!this._showQuotes
.length
) {
2339 this._showQuotes
= $('<div id="showQuotes" class="balloonTooltip" />').click($.proxy(this._click
, this)).appendTo(document
.body
);
2343 var $text
= WCF
.Language
.get('wcf.message.quote.showQuotes').replace(/#count#/, this._count
);
2344 this._showQuotes
.text($text
).show();
2347 this._hasTemplate
= false;
2351 * Handles clicks on 'Show quotes'.
2353 _click: function() {
2354 if (this._hasTemplate
) {
2355 this._dialog
.wcfDialog('open');
2358 this._proxy
.showLoadingOverlayOnce();
2360 this._proxy
.setOption('data', {
2361 actionName
: 'getQuotes',
2362 supportPaste
: this._supportPaste
2364 this._proxy
.sendRequest();
2369 * Renders the dialog.
2371 * @param string template
2373 renderDialog: function(template
) {
2374 // create dialog if not exists
2375 if (this._dialog
=== null) {
2376 this._dialog
= $('#messageQuoteList');
2377 if (!this._dialog
.length
) {
2378 this._dialog
= $('<div id="messageQuoteList" />').hide().appendTo(document
.body
);
2383 this._dialog
.html(template
);
2385 // add 'insert' and 'delete' buttons
2386 var $formSubmit
= $('<div class="formSubmit" />').appendTo(this._dialog
);
2387 if (this._supportPaste
) this._buttons
.insert
= $('<button class="buttonPrimary">' + WCF
.Language
.get('wcf.message.quote.insertAllQuotes') + '</button>').click($.proxy(this._insertSelected
, this)).appendTo($formSubmit
);
2388 this._buttons
.remove
= $('<button>' + WCF
.Language
.get('wcf.message.quote.removeAllQuotes') + '</button>').click($.proxy(this._removeSelected
, this)).appendTo($formSubmit
);
2391 this._dialog
.wcfDialog({
2392 title
: WCF
.Language
.get('wcf.message.quote.manageQuotes')
2394 this._dialog
.wcfDialog('render');
2395 this._hasTemplate
= true;
2397 // bind event listener
2398 var $insertQuoteButtons
= this._dialog
.find('.jsInsertQuote');
2399 if (this._supportPaste
) {
2400 $insertQuoteButtons
.click($.proxy(this._insertQuote
, this));
2403 $insertQuoteButtons
.hide();
2406 this._dialog
.find('input.jsCheckbox').change($.proxy(this._changeButtons
, this));
2408 // mark quotes for removal
2409 if (this._removeOnSubmit
.length
) {
2411 this._dialog
.find('input.jsRemoveQuote').each(function(index
, input
) {
2412 var $input
= $(input
).change($.proxy(this._change
, this));
2414 // mark for deletion
2415 if (WCF
.inArray($input
.parent('li').attr('data-quote-id'), self
._removeOnSubmit
)) {
2416 $input
.attr('checked', 'checked');
2423 * Updates button labels if a checkbox is checked or unchecked.
2425 _changeButtons: function() {
2427 if (this._dialog
.find('input.jsCheckbox:checked').length
) {
2428 if (this._supportPaste
) this._buttons
.insert
.html(WCF
.Language
.get('wcf.message.quote.insertSelectedQuotes'));
2429 this._buttons
.remove
.html(WCF
.Language
.get('wcf.message.quote.removeSelectedQuotes'));
2432 // no selection, pick all
2433 if (this._supportPaste
) this._buttons
.insert
.html(WCF
.Language
.get('wcf.message.quote.insertAllQuotes'));
2434 this._buttons
.remove
.html(WCF
.Language
.get('wcf.message.quote.removeAllQuotes'));
2439 * Checks for change event on delete-checkboxes.
2441 * @param object event
2443 _change: function(event
) {
2444 var $input
= $(event
.currentTarget
);
2445 var $quoteID
= $input
.parent('li').attr('data-quote-id');
2447 if ($input
.prop('checked')) {
2448 this._removeOnSubmit
.push($quoteID
);
2451 for (var $index
in this._removeOnSubmit
) {
2452 if (this._removeOnSubmit
[$index
] == $quoteID
) {
2453 delete this._removeOnSubmit
[$index
];
2461 * Inserts the selected quotes.
2463 _insertSelected: function() {
2464 if (this._editorElementAlternative
=== null) {
2465 var $api
= $('.jsQuickReply:eq(0)').data('__api');
2466 if ($api
&& !$api
.getContainer().is(':visible')) {
2467 this._insertQuotes
= false;
2472 if (!this._dialog
.find('input.jsCheckbox:checked').length
) {
2473 this._dialog
.find('input.jsCheckbox').prop('checked', 'checked');
2476 // insert all quotes
2477 this._dialog
.find('input.jsCheckbox:checked').each($.proxy(function(index
, input
) {
2478 this._insertQuote(null, input
);
2482 this._dialog
.wcfDialog('close');
2488 * @param object event
2489 * @param object inputElement
2491 _insertQuote: function(event
, inputElement
) {
2492 if (event
!== null && this._editorElementAlternative
=== null) {
2493 var $api
= $('.jsQuickReply:eq(0)').data('__api');
2494 if ($api
&& !$api
.getContainer().is(':visible')) {
2495 this._insertQuotes
= false;
2500 var $listItem
= (event
=== null) ? $(inputElement
).parents('li') : $(event
.currentTarget
).parents('li');
2501 var $quote
= $.trim($listItem
.children('div.jsFullQuote').text());
2502 var $message
= $listItem
.parents('article.message');
2505 $quote
= "[quote='" + $message
.attr('data-username') + "','" + $message
.data('link') + "']" + $quote
+ "[/quote]";
2507 // insert into editor
2508 if ($.browser
.redactor
) {
2509 if (this._editorElementAlternative
=== null) {
2510 this._editorElement
.redactor('insertDynamic', $quote
);
2513 this._editorElementAlternative
.redactor('insertDynamic', $quote
);
2518 var $textarea
= (this._editorElementAlternative
=== null) ? this._editorElement
: this._editorElementAlternative
;
2519 var $value
= $textarea
.val();
2521 if ($value
.length
== 0) {
2522 $textarea
.val($quote
);
2525 var $position
= $textarea
.getCaret();
2526 $textarea
.val( $value
.substr(0, $position
) + $quote
+ $value
.substr($position
) );
2530 // remove quote upon submit or upon request
2531 this._removeOnSubmit
.push($listItem
.attr('data-quote-id'));
2534 if (event
!== null) {
2535 this._dialog
.wcfDialog('close');
2540 * Removes selected quotes.
2542 _removeSelected: function() {
2543 if (!this._dialog
.find('input.jsCheckbox:checked').length
) {
2544 this._dialog
.find('input.jsCheckbox').prop('checked', 'checked');
2547 var $quoteIDs
= [ ];
2548 this._dialog
.find('input.jsCheckbox:checked').each(function(index
, input
) {
2549 $quoteIDs
.push($(input
).parents('li').attr('data-quote-id'));
2552 if ($quoteIDs
.length
) {
2554 var $objectTypes
= [ ];
2555 for (var $objectType
in this._handlers
) {
2556 $objectTypes
.push($objectType
);
2559 this._proxy
.setOption('data', {
2560 actionName
: 'remove',
2561 getFullQuoteObjectIDs
: this._handlers
.length
> 0,
2562 objectTypes
: $objectTypes
,
2565 this._proxy
.sendRequest();
2567 this._dialog
.wcfDialog('close');
2572 * Appends list of quote ids to remove after successful submit.
2574 _submit: function() {
2575 if (this._supportPaste
&& this._removeOnSubmit
.length
> 0) {
2576 var $formSubmit
= this._form
.find('.formSubmit');
2577 for (var $i
in this._removeOnSubmit
) {
2578 $('<input type="hidden" name="__removeQuoteIDs[]" value="' + this._removeOnSubmit
[$i
] + '" />').appendTo($formSubmit
);
2584 * Returns a list of quote ids marked for removal.
2586 * @return array<integer>
2588 getQuotesMarkedForRemoval: function() {
2589 return this._removeOnSubmit
;
2593 * Marks quote ids for removal.
2595 markQuotesForRemoval: function() {
2596 if (this._removeOnSubmit
.length
) {
2597 this._proxy
.setOption('data', {
2598 actionName
: 'markForRemoval',
2599 quoteIDs
: this._removeOnSubmit
2601 this._proxy
.suppressErrors();
2602 this._proxy
.sendRequest();
2607 * Removes all marked quote ids.
2609 removeMarkedQuotes: function() {
2610 if (this._removeOnSubmit
.length
) {
2611 this._proxy
.setOption('data', {
2612 actionName
: 'removeMarkedQuotes',
2613 getFullQuoteObjectIDs
: this._handlers
.length
> 0
2615 this._proxy
.sendRequest();
2620 * Counts stored quotes.
2622 countQuotes: function() {
2623 var $objectTypes
= [ ];
2624 for (var $objectType
in this._handlers
) {
2625 $objectTypes
.push($objectType
);
2628 this._proxy
.setOption('data', {
2629 actionName
: 'count',
2630 getFullQuoteObjectIDs
: this._handlers
.length
> 0,
2631 objectTypes
: $objectTypes
2633 this._proxy
.sendRequest();
2637 * Handles successful AJAX requests.
2639 * @param object data
2640 * @param string textStatus
2641 * @param jQuery jqXHR
2643 _success: function(data
, textStatus
, jqXHR
) {
2644 if (data
=== null) {
2648 if (data
.count
!== undefined) {
2649 var $fullQuoteObjectIDs
= (data
.fullQuoteObjectIDs
!== undefined) ? data
.fullQuoteObjectIDs
: { };
2650 this.updateCount(data
.count
, $fullQuoteObjectIDs
);
2653 if (data
.template
!== undefined) {
2654 if ($.trim(data
.template
) == '') {
2655 this.updateCount(0, { });
2658 this.renderDialog(data
.template
);
2665 * Namespace for message sharing related classes.
2667 WCF
.Message
.Share
= { };
2670 * Displays a dialog overlay for permalinks.
2672 WCF
.Message
.Share
.Content
= Class
.extend({
2674 * list of cached templates
2686 * Initializes the WCF.Message.Share.Content class.
2690 this._dialog
= null;
2694 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.Share.Content', $.proxy(this._initLinks
, this));
2698 * Initializes share links.
2700 _initLinks: function() {
2701 $('a.jsButtonShare').removeClass('jsButtonShare').click($.proxy(this._click
, this));
2705 * Displays links to share this content.
2707 * @param object event
2709 _click: function(event
) {
2710 event
.preventDefault();
2712 var $target
= $(event
.currentTarget
);
2713 var $link
= $target
.prop('href');
2714 var $title
= ($target
.data('linkTitle') ? $target
.data('linkTitle') : $link
);
2715 var $key
= $link
.hashCode();
2716 if (this._cache
[$key
] === undefined) {
2717 // remove dialog contents
2718 var $dialogInitialized
= false;
2719 if (this._dialog
=== null) {
2720 this._dialog
= $('<div />').hide().appendTo(document
.body
);
2721 $dialogInitialized
= true;
2724 this._dialog
.empty();
2727 // permalink (plain text)
2728 var $fieldset
= $('<fieldset><legend><label for="__sharePermalink">' + WCF
.Language
.get('wcf.message.share.permalink') + '</label></legend></fieldset>').appendTo(this._dialog
);
2729 $('<input type="text" id="__sharePermalink" class="long" readonly="readonly" />').attr('value', $link
).appendTo($fieldset
);
2731 // permalink (BBCode)
2732 var $fieldset
= $('<fieldset><legend><label for="__sharePermalinkBBCode">' + WCF
.Language
.get('wcf.message.share.permalink.bbcode') + '</label></legend></fieldset>').appendTo(this._dialog
);
2733 $('<input type="text" id="__sharePermalinkBBCode" class="long" readonly="readonly" />').attr('value', '[url=\'' + $link
+ '\']' + $title
+ '[/url]').appendTo($fieldset
);
2736 var $fieldset
= $('<fieldset><legend><label for="__sharePermalinkHTML">' + WCF
.Language
.get('wcf.message.share.permalink.html') + '</label></legend></fieldset>').appendTo(this._dialog
);
2737 $('<input type="text" id="__sharePermalinkHTML" class="long" readonly="readonly" />').attr('value', '<a href="' + $link
+ '">' + WCF
.String
.escapeHTML($title
) + '</a>').appendTo($fieldset
);
2739 this._cache
[$key
] = this._dialog
.html();
2741 if ($dialogInitialized
) {
2742 this._dialog
.wcfDialog({
2743 title
: WCF
.Language
.get('wcf.message.share')
2747 this._dialog
.wcfDialog('open');
2751 this._dialog
.html(this._cache
[$key
]).wcfDialog('open');
2754 this._enableSelection();
2758 * Enables text selection.
2760 _enableSelection: function() {
2761 var $inputElements
= this._dialog
.find('input').click(function() { $(this).select(); });
2763 // Safari on iOS can only select the text if it is not readonly and setSelectionRange() is used
2764 if (navigator
.userAgent
.match(/iP(ad|hone|od)/)) {
2765 $inputElements
.keydown(function() { return false; }).removeAttr('readonly').click(function() { this.setSelectionRange(0, 9999); });
2771 * Provides buttons to share a page through multiple social community sites.
2773 * @param boolean fetchObjectCount
2775 WCF
.Message
.Share
.Page
= Class
.extend({
2777 * list of share buttons
2786 _pageDescription
: '',
2789 * canonical page URL
2795 * Initializes the WCF.Message.Share.Page class.
2797 * @param boolean fetchObjectCount
2799 init: function(fetchObjectCount
) {
2800 this._pageDescription
= encodeURIComponent($('meta[property="og:title"]').prop('content'));
2801 this._pageURL
= encodeURIComponent($('meta[property="og:url"]').prop('content'));
2803 var $container
= $('.messageShareButtons');
2805 facebook
: $container
.find('.jsShareFacebook'),
2806 google
: $container
.find('.jsShareGoogle'),
2807 reddit
: $container
.find('.jsShareReddit'),
2808 twitter
: $container
.find('.jsShareTwitter')
2811 this._ui
.facebook
.children('a').click($.proxy(this._shareFacebook
, this));
2812 this._ui
.google
.children('a').click($.proxy(this._shareGoogle
, this));
2813 this._ui
.reddit
.children('a').click($.proxy(this._shareReddit
, this));
2814 this._ui
.twitter
.children('a').click($.proxy(this._shareTwitter
, this));
2816 if (fetchObjectCount
=== true) {
2817 this._fetchFacebook();
2818 this._fetchTwitter();
2819 this._fetchReddit();
2824 * Shares current page to selected social community site.
2826 * @param string objectName
2828 * @param boolean appendURL
2830 _share: function(objectName
, url
, appendURL
) {
2831 window
.open(url
.replace(/{pageURL}/, this._pageURL
).replace(/{text}/, this._pageDescription
+ (appendURL
? " " + this._pageURL
: "")), 'height=600,width=600');
2835 * Shares current page with Facebook.
2837 _shareFacebook: function() {
2838 this._share('facebook', 'https://www.facebook.com/sharer.php?u={pageURL}&t={text}', true);
2842 * Shares current page with Google Plus.
2844 _shareGoogle: function() {
2845 this._share('google', 'https://plus.google.com/share?url={pageURL}', true);
2849 * Shares current page with Reddit.
2851 _shareReddit: function() {
2852 this._share('reddit', 'https://ssl.reddit.com/submit?url={pageURL}', true);
2856 * Shares current page with Twitter.
2858 _shareTwitter: function() {
2859 this._share('twitter', 'https://twitter.com/share?url={pageURL}&text={text}', false);
2863 * Fetches share count from a social community site.
2866 * @param object callback
2867 * @param string callbackName
2869 _fetchCount: function(url
, callback
, callbackName
) {
2873 showLoadingOverlay
: false,
2875 suppressErrors
: true,
2877 url
: url
.replace(/{pageURL}/, this._pageURL
)
2880 $options
.jsonp
= callbackName
;
2883 new WCF
.Action
.Proxy($options
);
2887 * Fetches number of Facebook likes.
2889 _fetchFacebook: function() {
2890 this._fetchCount('https://graph.facebook.com/?id={pageURL}', $.proxy(function(data
) {
2892 this._ui
.facebook
.children('span.badge').show().text(data
.shares
);
2898 * Fetches tweet count from Twitter.
2900 _fetchTwitter: function() {
2901 if (window
.location
.protocol
.match(/^https/)) return;
2903 this._fetchCount('http://urls.api.twitter.com/1/urls/count.json?url={pageURL}', $.proxy(function(data
) {
2905 this._ui
.twitter
.children('span.badge').show().text(data
.count
);
2911 * Fetches cumulative vote sum from Reddit.
2913 _fetchReddit: function() {
2914 if (window
.location
.protocol
.match(/^https/)) return;
2916 this._fetchCount('http://www.reddit.com/api/info.json?url={pageURL}', $.proxy(function(data
) {
2917 if (data
.data
.children
.length
) {
2918 this._ui
.reddit
.children('span.badge').show().text(data
.data
.children
[0].data
.score
);
2925 * Handles user mention suggestions in Redactor instances.
2927 * Important: Objects of this class have to be created before the CKEditor
2930 WCF
.Message
.UserMention
= Class
.extend({
2932 * current caret position
2935 _caretPosition
: null,
2938 * name of the class used to get the user suggestions
2941 _className
: 'wcf\\data\\user\\UserAction',
2950 * dropdown menu object
2953 _dropdownMenu
: null,
2956 * suggestion item index, -1 if none is selected
2968 * current beginning of the mentioning
2974 * redactor instance object
2980 * Initalizes user suggestions for the CKEditor with the given textarea id.
2982 * @param string wysiwygSelector
2984 init: function(wysiwygSelector
) {
2985 this._textarea
= $('#' + wysiwygSelector
);
2986 this._redactor
= this._textarea
.redactor('getObject');
2988 this._redactor
.setOption('keyupCallback', $.proxy(this._keyup
, this));
2989 this._redactor
.setOption('wkeydownCallback', $.proxy(this._keydown
, this));
2991 this._dropdown
= this._textarea
.redactor('getEditor');
2992 this._dropdownMenu
= $('<ul class="dropdownMenu userSuggestionList" />').appendTo(this._textarea
.parent());
2993 WCF
.Dropdown
.initDropdownFragment(this._dropdown
, this._dropdownMenu
);
2995 this._proxy
= new WCF
.Action
.Proxy({
2996 success
: $.proxy(this._success
, this)
3001 * Clears the suggestion list.
3003 _clearList: function() {
3006 this._dropdownMenu
.empty();
3010 * Handles a click on a list item suggesting a username.
3012 * @param object event
3014 _click: function(event
) {
3015 // restore caret position
3016 this._redactor
.replaceRangesWith(this._caretPosition
);
3018 this._setUsername($(event
.currentTarget
).data('username'));
3022 * Creates an item in the suggestion list with the given data.
3026 _createListItem: function(listItemData
) {
3027 var $listItem
= $('<li />').data('username', listItemData
.label
).click($.proxy(this._click
, this)).appendTo(this._dropdownMenu
);
3029 var $box16
= $('<div />').addClass('box16').appendTo($listItem
);
3030 $box16
.append($(listItemData
.icon
).addClass('framed'));
3031 $box16
.append($('<div />').append($('<span />').text(listItemData
.label
)));
3035 * Returns the offsets used to set the position of the user suggestion
3040 _getDropdownMenuPosition: function() {
3041 var $orgRange
= getSelection().getRangeAt(0).cloneRange();
3043 // mark the entire text, starting from the '@' to the current cursor position
3044 var $newRange
= document
.createRange();
3045 $newRange
.setStart($orgRange
.startContainer
, $orgRange
.startOffset
- (this._mentionStart
.length
+ 1));
3046 $newRange
.setEnd($orgRange
.startContainer
, $orgRange
.startOffset
);
3048 this._redactor
.replaceRangesWith($newRange
);
3050 // get the offsets of the bounding box of current text selection
3051 var $range
= getSelection().getRangeAt(0);
3052 var $rect
= $range
.getBoundingClientRect();
3053 var $window
= $(window
);
3055 top
: Math
.round($rect
.bottom
) + $window
.scrollTop(),
3056 left
: Math
.round($rect
.left
) + $window
.scrollLeft()
3059 if (this._lineHeight
=== null) {
3060 this._lineHeight
= Math
.round($rect
.bottom
- $rect
.top
);
3063 // restore caret position
3064 this._redactor
.replaceRangesWith($orgRange
);
3065 this._caretPosition
= $orgRange
;
3071 * Replaces the started mentioning with a chosen username.
3073 _setUsername: function(username
) {
3074 var $orgRange
= getSelection().getRangeAt(0).cloneRange();
3076 // allow redactor to undo this
3077 this._redactor
.bufferSet();
3079 var $newRange
= document
.createRange();
3080 $newRange
.setStart($orgRange
.startContainer
, $orgRange
.startOffset
- (this._mentionStart
.length
+ 1));
3081 $newRange
.setEnd($orgRange
.startContainer
, $orgRange
.startOffset
);
3083 this._redactor
.replaceRangesWith($newRange
);
3085 var $range
= getSelection().getRangeAt(0);
3086 $range
.deleteContents();
3087 $range
.collapse(true);
3090 if (username
.indexOf("'") !== -1) {
3091 username
= username
.replace(/'/g, "''");
3092 username = "'" + username + "'";
3094 else if (username.indexOf(' ') !== -1) {
3095 username = "'" + username + "'";
3098 // use native API to prevent issues in Internet Explorer
3099 var $text = document.createTextNode('@' + username);
3100 $range.insertNode($text);
3102 var $newRange = document.createRange();
3103 $newRange.setStart($text, username.length + 1);
3104 $newRange.setEnd($text, username.length + 1);
3106 this._redactor.replaceRangesWith($newRange);
3112 * Returns the parameters for the AJAX request.
3116 _getParameters: function() {
3119 includeUserGroups: false,
3120 searchString: this._mentionStart
3126 * Returns the relevant text in front of the caret in the current line.
3130 _getTextLineInFrontOfCaret: function() {
3131 // if text is marked, user suggestions are disabled
3132 if (this._redactor.getSelectionHtml().length) {
3136 var $range = this._redactor.getSelection().getRangeAt(0);
3137 var $text = $range.startContainer.textContent.substr(0, $range.startOffset);
3139 // remove unicode zero width space and non-breaking space
3140 var $textBackup = $text;
3142 for (var $i = 0; $i < $textBackup.length; $i++) {
3143 var $byte = $textBackup.charCodeAt($i).toString(16);
3144 if ($byte != '200b
' && !/\s/.test($textBackup[$i])) {
3145 if ($textBackup[$i] === '@' && $i && /\s/.test($textBackup[$i - 1])) {
3149 $text += $textBackup[$i];
3160 * Hides the suggestion list.
3162 _hideList: function() {
3163 this._dropdown.removeClass('dropdownOpen
');
3164 this._dropdownMenu.removeClass('dropdownOpen
');
3166 this._itemIndex = -1;
3170 * Handles the keydown event to check if the user starts mentioning someone.
3172 * @param object event
3174 _keydown: function(event) {
3175 if (this._redactor.inPlainMode()) {
3179 if (this._dropdownMenu.is(':visible
')) {
3180 switch (event.which) {
3181 case $.ui.keyCode.ENTER:
3182 event.preventDefault();
3184 this._dropdownMenu.children('li
').eq(this._itemIndex).trigger('click
');
3189 case $.ui.keyCode.UP:
3190 event.preventDefault();
3192 this._selectItem(this._itemIndex - 1);
3197 case $.ui.keyCode.DOWN:
3198 event.preventDefault();
3200 this._selectItem(this._itemIndex + 1);
3211 * Handles the keyup event to check if the user starts mentioning someone.
3213 * @param object event
3215 _keyup: function(event) {
3216 if (this._redactor.inPlainMode()) {
3220 // ignore enter key up event
3221 if (event.which === $.ui.keyCode.ENTER) {
3225 // ignore event if suggestion list and user pressed enter, arrow up or arrow down
3226 if (this._dropdownMenu.is(':visible
') && event.which in { 13:1, 38:1, 40:1 }) {
3230 var $currentText = this._getTextLineInFrontOfCaret();
3232 var $match = $currentText.match(/@([^,]{3,})$/);
3234 // if mentioning is at text begin or there's a whitespace character
3235 // before the '@', everything is fine
3236 if (!$match
.index
|| $currentText
[$match
.index
- 1].match(/\s/)) {
3237 this._mentionStart
= $match
[1];
3239 this._proxy
.setOption('data', {
3240 actionName
: 'getSearchResultList',
3241 className
: this._className
,
3242 interfaceName
: 'wcf\\data\\ISearchAction',
3243 parameters
: this._getParameters()
3245 this._proxy
.sendRequest();
3258 * Selects the suggestion with the given item index.
3260 * @param integer itemIndex
3262 _selectItem: function(itemIndex
) {
3263 var $li
= this._dropdownMenu
.children('li');
3265 if (itemIndex
< 0) {
3266 itemIndex
= $li
.length
- 1;
3268 else if (itemIndex
+ 1 > $li
.length
) {
3272 $li
.removeClass('dropdownNavigationItem');
3273 $li
.eq(itemIndex
).addClass('dropdownNavigationItem');
3275 this._itemIndex
= itemIndex
;
3279 * Shows the suggestion list.
3281 _showList: function() {
3282 this._dropdown
.addClass('dropdownOpen');
3283 this._dropdownMenu
.addClass('dropdownOpen');
3287 * Evalutes user suggestion-AJAX request results.
3289 * @param object data
3290 * @param string textStatus
3291 * @param jQuery jqXHR
3293 _success: function(data
, textStatus
, jqXHR
) {
3294 this._clearList(false);
3296 if ($.getLength(data
.returnValues
)) {
3297 for (var $i
in data
.returnValues
) {
3298 var $item
= data
.returnValues
[$i
];
3299 this._createListItem($item
);
3302 this._updateSuggestionListPosition();
3308 * Updates the position of the suggestion list.
3310 _updateSuggestionListPosition: function() {
3312 var $dropdownMenuPosition
= this._getDropdownMenuPosition();
3313 $dropdownMenuPosition
.top
+= 5; // add a little vertical gap
3315 this._dropdownMenu
.css($dropdownMenuPosition
);
3316 this._selectItem(0);
3318 if ($dropdownMenuPosition
.top
+ this._dropdownMenu
.outerHeight() + 10 > $(window
).height() + $(document
).scrollTop()) {
3319 this._dropdownMenu
.addClass('dropdownArrowBottom');
3321 this._dropdownMenu
.css({
3322 top
: $dropdownMenuPosition
.top
- this._dropdownMenu
.outerHeight() - 2 * this._lineHeight
+ 5
3326 this._dropdownMenu
.removeClass('dropdownArrowBottom');
3330 // ignore errors that are caused by pressing enter to
3331 // often in a short period of time