4 * Message related classes for WCF
6 * @author Alexander Ebert
7 * @copyright 2001-2014 WoltLab GmbH
8 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
13 * Namespace for BBCode related classes.
15 WCF
.Message
.BBCode
= { };
18 * BBCode Viewer for WCF.
20 WCF
.Message
.BBCode
.CodeViewer
= Class
.extend({
28 * Initializes the WCF.Message.BBCode.CodeViewer class.
33 this._initCodeBoxes();
35 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.BBCode.CodeViewer', $.proxy(this._initCodeBoxes
, this));
36 WCF
.DOMNodeInsertedHandler
.execute();
40 * Initializes available code boxes.
42 _initCodeBoxes: function() {
43 $('.codeBox:not(.jsCodeViewer)').each($.proxy(function(index
, codeBox
) {
44 var $codeBox
= $(codeBox
).addClass('jsCodeViewer');
46 $('<span class="icon icon16 icon-copy pointer jsTooltip" title="' + WCF
.Language
.get('wcf.message.bbcode.code.copy') + '" />').appendTo($codeBox
.find('div > h3')).click($.proxy(this._click
, this));
51 * Shows a code viewer for a specific code box.
55 _click: function(event
) {
57 $(event
.currentTarget
).parents('div').next('ol').children('li').each(function(index
, listItem
) {
62 // do *not* use $.trim here, as we want to preserve whitespaces
63 $content
+= $(listItem
).text().replace(/\n+$/, '');
66 if (this._dialog
=== null) {
67 this._dialog
= $('<div><textarea cols="60" rows="12" readonly="readonly" /></div>').hide().appendTo(document
.body
);
68 this._dialog
.children('textarea').val($content
);
69 this._dialog
.wcfDialog({
70 title
: WCF
.Language
.get('wcf.message.bbcode.code.copy')
74 this._dialog
.children('textarea').val($content
);
75 this._dialog
.wcfDialog('open');
78 this._dialog
.children('textarea').select();
83 * Provides the dynamic parts of the edit history interface.
85 WCF
.Message
.EditHistory
= Class
.extend({
87 * jQuery object containing the radio buttons for the oldID
93 * jQuery object containing the radio buttons for the oldID
99 * selector for the version rows
102 _containerSelector
: '',
105 * selector for the revert button
108 _buttonSelector
: '.jsRevertButton',
111 * Initializes the edit history interface.
113 * @param object oldIDInputs
114 * @param object newIDInputs
115 * @param string containerSelector
116 * @param string buttonSelector
118 init: function(oldIDInputs
, newIDInputs
, containerSelector
, buttonSelector
) {
119 this._oldIDInputs
= oldIDInputs
;
120 this._newIDInputs
= newIDInputs
;
121 this._containerSelector
= containerSelector
;
122 this._buttonSelector
= (buttonSelector
) ? buttonSelector
: '.jsRevertButton';
124 this.proxy
= new WCF
.Action
.Proxy({
125 success
: $.proxy(this._success
, this)
129 this._initElements();
133 * Initializes the radio buttons.
134 * Force the "oldID" to be lower than the "newID"
135 * 'current' is interpreted as Infinity.
137 _initInputs: function() {
139 this._newIDInputs
.change(function(event
) {
140 var newID
= parseInt($(this).val());
141 if ($(this).val() === 'current') newID
= Infinity
;
143 self
._oldIDInputs
.each(function(event
) {
144 var oldID
= parseInt($(this).val());
145 if ($(this).val() === 'current') oldID
= Infinity
;
147 if (oldID
>= newID
) {
156 this._oldIDInputs
.change(function(event
) {
157 var oldID
= parseInt($(this).val());
158 if ($(this).val() === 'current') oldID
= Infinity
;
160 self
._newIDInputs
.each(function(event
) {
161 var newID
= parseInt($(this).val());
162 if ($(this).val() === 'current') newID
= Infinity
;
164 if (newID
<= oldID
) {
172 this._oldIDInputs
.filter(':checked').change();
173 this._newIDInputs
.filter(':checked').change();
177 * Initializes available element containers.
179 _initElements: function() {
181 $(this._containerSelector
).each(function(index
, container
) {
182 var $container
= $(container
);
183 $container
.find(self
._buttonSelector
).click($.proxy(self
._click
, self
));
188 * Sends AJAX request.
190 * @param object event
192 _click: function(event
) {
193 var $target
= $(event
.currentTarget
);
194 event
.preventDefault();
196 if ($target
.data('confirmMessage')) {
199 WCF
.System
.Confirmation
.show($target
.data('confirmMessage'), function(action
) {
200 if (action
=== 'cancel') return;
202 self
._sendRequest($target
);
206 this._sendRequest($target
);
214 * @param jQuery object
216 _sendRequest: function(object
) {
217 this.proxy
.setOption('data', {
218 actionName
: 'revert',
219 className
: 'wcf\\data\\edit\\history\\entry\\EditHistoryEntryAction',
220 objectIDs
: [ $(object
).data('objectID') ]
223 this.proxy
.sendRequest();
227 * Reloads the page to show the new versions.
230 * @param string textStatus
231 * @param object jqXHR
233 _success: function(data
, textStatus
, jqXHR
) {
234 window
.location
.reload(true);
239 * Prevents multiple submits of the same form by disabling the submit button.
241 WCF
.Message
.FormGuard
= Class
.extend({
243 * Initializes the WCF.Message.FormGuard class.
246 var $forms
= $('form.jsFormGuard').removeClass('jsFormGuard').submit(function() {
247 $(this).find('.formSubmit input[type=submit]').disable();
250 // restore buttons, prevents disabled buttons on back navigation in Opera
251 $(window
).unload(function() {
252 $forms
.find('.formSubmit input[type=submit]').enable();
258 * Provides previews for Redactor message fields.
260 * @param string className
261 * @param string messageFieldID
262 * @param string previewButtonID
264 WCF
.Message
.Preview
= Class
.extend({
285 * @var WCF.Action.Proxy
293 _previewButton
: null,
296 * previous button label
299 _previewButtonLabel
: '',
302 * Initializes a new WCF.Message.Preview object.
304 * @param string className
305 * @param string messageFieldID
306 * @param string previewButtonID
308 init: function(className
, messageFieldID
, previewButtonID
) {
309 this._className
= className
;
311 // validate message field
312 this._messageFieldID
= $.wcfEscapeID(messageFieldID
);
313 this._messageField
= $('#' + this._messageFieldID
);
314 if (!this._messageField
.length
) {
315 console
.debug("[WCF.Message.Preview] Unable to find message field identified by '" + this._messageFieldID
+ "'");
319 // validate preview button
320 previewButtonID
= $.wcfEscapeID(previewButtonID
);
321 this._previewButton
= $('#' + previewButtonID
);
322 if (!this._previewButton
.length
) {
323 console
.debug("[WCF.Message.Preview] Unable to find preview button identified by '" + previewButtonID
+ "'");
327 this._previewButton
.click($.proxy(this._click
, this));
328 this._proxy
= new WCF
.Action
.Proxy({
329 failure
: $.proxy(this._failure
, this),
330 success
: $.proxy(this._success
, this)
335 * Reads message field input and triggers an AJAX request.
337 _click: function(event
) {
338 var $message
= this._getMessage();
339 if ($message
=== null) {
340 console
.debug("[WCF.Message.Preview] Unable to access Redactor instance of '" + this._messageFieldID
+ "'");
344 this._proxy
.setOption('data', {
345 actionName
: 'getMessagePreview',
346 className
: this._className
,
347 parameters
: this._getParameters($message
)
349 this._proxy
.sendRequest();
351 // update button label
352 this._previewButtonLabel
= this._previewButton
.html();
353 this._previewButton
.html(WCF
.Language
.get('wcf.global.loading')).disable();
356 event
.stopPropagation();
361 * Returns request parameters.
363 * @param string message
366 _getParameters: function(message
) {
367 // collect message form options
369 $('#settings_' + this._messageFieldID
).find('input[type=checkbox]').each(function(index
, checkbox
) {
370 var $checkbox
= $(checkbox
);
371 if ($checkbox
.is(':checked')) {
372 $options
[$checkbox
.prop('name')] = $checkbox
.prop('value');
386 * Returns parsed message from Redactor or null if editor was not accessible.
390 _getMessage: function() {
391 if (!$.browser
.redactor
) {
392 return $.trim(this._messageField
.val());
394 else if (this._messageField
.data('redactor')) {
395 return this._messageField
.redactor('wutil.getText');
402 * Handles successful AJAX requests.
405 * @param string textStatus
406 * @param jQuery jqXHR
408 _success: function(data
, textStatus
, jqXHR
) {
409 // restore preview button
410 this._previewButton
.html(this._previewButtonLabel
).enable();
412 // remove error message
413 this._messageField
.parent().children('small.innerError').remove();
416 this._handleResponse(data
);
420 * Evaluates response data.
424 _handleResponse: function(data
) { },
427 * Handles errors during preview requests.
429 * The return values indicates if the default error overlay is shown.
434 _failure: function(data
) {
435 if (data
=== null || data
.returnValues
=== undefined || data
.returnValues
.errorType
=== undefined) {
439 // restore preview button
440 this._previewButton
.html(this._previewButtonLabel
).enable();
442 var $innerError
= this._messageField
.next('small.innerError').empty();
443 if (!$innerError
.length
) {
444 $innerError
= $('<small class="innerError" />').appendTo(this._messageField
.parent());
447 $innerError
.html(data
.returnValues
.errorType
);
454 * Default implementation for message previews.
456 * @see WCF.Message.Preview
458 WCF
.Message
.DefaultPreview
= WCF
.Message
.Preview
.extend({
459 _attachmentObjectType
: null,
460 _attachmentObjectID
: null,
464 * @see WCF.Message.Preview.init()
466 init: function(attachmentObjectType
, attachmentObjectID
, tmpHash
) {
467 this._super('wcf\\data\\bbcode\\MessagePreviewAction', 'text', 'previewButton');
469 this._attachmentObjectType
= attachmentObjectType
|| null;
470 this._attachmentObjectID
= attachmentObjectID
|| null;
471 this._tmpHash
= tmpHash
|| null;
475 * @see WCF.Message.Preview._handleResponse()
477 _handleResponse: function(data
) {
478 var $preview
= $('#previewContainer');
479 if (!$preview
.length
) {
480 $preview
= $('<div class="container containerPadding marginTop" id="previewContainer"><fieldset><legend>' + WCF
.Language
.get('wcf.global.preview') + '</legend><div></div></fieldset>').prependTo($('#messageContainer')).wcfFadeIn();
483 $preview
.find('div:eq(0)').html(data
.returnValues
.message
);
485 new WCF
.Effect
.Scroll().scrollTo($preview
);
489 * @see WCF.Message.Preview._getParameters()
491 _getParameters: function(message
) {
492 var $parameters
= this._super(message
);
494 if (this._attachmentObjectType
!= null) {
495 $parameters
.attachmentObjectType
= this._attachmentObjectType
;
496 $parameters
.attachmentObjectID
= this._attachmentObjectID
;
497 $parameters
.tmpHash
= this._tmpHash
;
505 * Handles multilingualism for messages.
507 * @param integer languageID
508 * @param object availableLanguages
509 * @param boolean forceSelection
511 WCF
.Message
.Multilingualism
= Class
.extend({
513 * list of available languages
516 _availableLanguages
: { },
525 * language input element
528 _languageInput
: null,
531 * Initializes WCF.Message.Multilingualism
533 * @param integer languageID
534 * @param object availableLanguages
535 * @param boolean forceSelection
537 init: function(languageID
, availableLanguages
, forceSelection
) {
538 this._availableLanguages
= availableLanguages
;
539 this._languageID
= languageID
|| 0;
541 this._languageInput
= $('#languageID');
543 // preselect current language id
546 // register event listener
547 this._languageInput
.find('.dropdownMenu > li').click($.proxy(this._click
, this));
549 // add element to disable multilingualism
550 if (!forceSelection
) {
551 var $dropdownMenu
= this._languageInput
.find('.dropdownMenu');
552 $('<li class="dropdownDivider" />').appendTo($dropdownMenu
);
553 $('<li><span><span class="badge">' + this._availableLanguages
[0] + '</span></span></li>').click($.proxy(this._disable
, this)).appendTo($dropdownMenu
);
557 this._languageInput
.parents('form').submit($.proxy(this._submit
, this));
561 * Handles language selections.
563 * @param object event
565 _click: function(event
) {
566 this._languageID
= $(event
.currentTarget
).data('languageID');
571 * Disables language selection.
573 _disable: function() {
574 this._languageID
= 0;
579 * Updates selected language.
581 _updateLabel: function() {
582 this._languageInput
.find('.dropdownToggle > span').text(this._availableLanguages
[this._languageID
]);
586 * Sets language id upon submit.
588 _submit: function() {
589 this._languageInput
.next('input[name=languageID]').prop('value', this._languageID
);
594 * Loads smiley categories upon user request.
596 WCF
.Message
.SmileyCategories
= Class
.extend({
598 * list of already loaded category ids
599 * @var array<integer>
605 * @var WCF.Action.Proxy
610 * wysiwyg editor selector
613 _wysiwygSelector
: '',
616 * Initializes the smiley loader.
618 * @param string wysiwygSelector
620 init: function(wysiwygSelector
) {
621 this._proxy
= new WCF
.Action
.Proxy({
622 success
: $.proxy(this._success
, this)
624 this._wysiwygSelector
= wysiwygSelector
;
626 $('#smilies-' + this._wysiwygSelector
).on('messagetabmenushow', $.proxy(this._click
, this));
630 * Handles tab menu clicks.
632 * @param object event
635 _click: function(event
, data
) {
636 var $categoryID
= parseInt(data
.activeTab
.tab
.data('smileyCategoryID'));
638 // ignore global category, will always be pre-loaded
643 // smilies have already been loaded for this tab, ignore
644 if (data
.activeTab
.container
.children('ul.smileyList').length
) {
649 if (this._cache
[$categoryID
] !== undefined) {
650 data
.activeTab
.container
.html(this._cache
[$categoryID
]);
654 this._proxy
.setOption('data', {
655 actionName
: 'getSmilies',
656 className
: 'wcf\\data\\smiley\\category\\SmileyCategoryAction',
657 objectIDs
: [ $categoryID
]
659 this._proxy
.sendRequest();
663 * Handles successful AJAX requests.
666 * @param string textStatus
667 * @param jQuery jqXHR
669 _success: function(data
, textStatus
, jqXHR
) {
670 var $categoryID
= parseInt(data
.returnValues
.smileyCategoryID
);
671 this._cache
[$categoryID
] = data
.returnValues
.template
;
673 $('#smilies-' + this._wysiwygSelector
+ '-' + $categoryID
).html(data
.returnValues
.template
);
678 * Handles smiley clicks.
680 * @param string wysiwygSelector
682 WCF
.Message
.Smilies
= Class
.extend({
690 * wysiwyg container id
693 _wysiwygSelector
: '',
696 * Initializes the smiley handler.
698 * @param string wysiwygSelector
700 init: function(wysiwygSelector
) {
701 this._wysiwygSelector
= wysiwygSelector
;
703 WCF
.System
.Dependency
.Manager
.register('Redactor_' + this._wysiwygSelector
, $.proxy(function() {
704 this._redactor
= $('#' + this._wysiwygSelector
).redactor('core.getObject');
706 $('.messageTabMenu[data-wysiwyg-container-id=' + this._wysiwygSelector
+ ']').on('click', '.jsSmiley', $.proxy(this._smileyClick
, this));
711 * Handles tab smiley clicks.
713 * @param object event
715 _smileyClick: function(event
) {
716 var $target
= $(event
.currentTarget
);
717 var $smileyCode
= $target
.data('smileyCode');
718 var $smileyPath
= $target
.data('smileyPath');
721 this._redactor
.wbbcode
.insertSmiley($smileyCode
, $smileyPath
, true);
726 * Provides an AJAX-based quick reply for messages.
728 WCF
.Message
.QuickReply
= Class
.extend({
730 * quick reply container
742 * notification object
743 * @var WCF.System.Notification
748 * true, if a request to save the message is pending
755 * @var WCF.Action.Proxy
760 * collection of quick reply buttons
763 _quickReplyButtons
: null,
766 * quote manager object
767 * @var WCF.Message.Quote.Manager
773 * @var WCF.Effect.Scroll
775 _scrollHandler
: null,
778 * success message for created but invisible messages
781 _successMessageNonVisible
: '',
784 * Initializes a new WCF.Message.QuickReply object.
786 * @param boolean supportExtendedForm
787 * @param WCF.Message.Quote.Manager quoteManager
789 init: function(supportExtendedForm
, quoteManager
) {
790 this._container
= $('#messageQuickReply');
791 this._container
.children('.message').addClass('jsInvalidQuoteTarget');
792 this._messageField
= $('#text');
793 this._pendingSave
= false;
794 if (!this._container
|| !this._messageField
) {
799 var $formSubmit
= this._container
.find('.formSubmit');
800 var $saveButton
= $formSubmit
.find('button[data-type=save]').removeAttr('accesskey').click($.proxy(this._save
, this));
801 if (supportExtendedForm
) $formSubmit
.find('button[data-type=extended]').click($.proxy(this._prepareExtended
, this));
802 $formSubmit
.find('button[data-type=cancel]').click($.proxy(this._cancel
, this));
804 if (quoteManager
) this._quoteManager
= quoteManager
;
806 this._quickReplyButtons
= $('.jsQuickReply').data('__api', this).click($.proxy(this.click
, this));
808 this._proxy
= new WCF
.Action
.Proxy({
809 failure
: $.proxy(this._failure
, this),
810 showLoadingOverlay
: false,
811 success
: $.proxy(this._success
, this)
813 this._scroll
= new WCF
.Effect
.Scroll();
814 this._notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.global.success.add'));
815 this._successMessageNonVisible
= '';
817 WCF
.System
.Event
.addListener('com.woltlab.wcf.redactor', 'submitEditor_text', function(data
) {
820 $saveButton
.trigger('click');
823 WCF
.System
.Event
.addListener('com.woltlab.wcf.message.quote', 'insert', (function(data
) {
824 var $insertQuote
= false;
827 if (this._container
.is(':visible')) {
830 else if (data
.forceInsert
) {
831 // do not programmatically insert the quote because the callback will already do this
832 $insertQuote
= (this._messageField
.redactor('wutil.isEmptyEditor') ? false : true);
837 this._messageField
.redactor('wutil.selectionEndOfEditor');
838 this._messageField
.redactor('wbbcode.insertQuoteBBCode', data
.quote
.username
, data
.quote
.link
, data
.quote
.text
, data
.quote
.text
);
841 this._scroll
.scrollTo(this._container
, true);
847 * Handles clicks on reply button.
849 * @param object event
851 click: function(event
) {
852 this._container
.toggle();
854 if (this._container
.is(':visible')) {
855 this._quickReplyButtons
.hide();
857 setTimeout((function() {
858 $(document
).trigger('resize');
859 if (!$.browser
.mobile
|| !$.browser
.chrome
) {
860 // Chrome on Android scrolls to the caret position, manually scrolling breaks the position
861 this._scroll
.scrollTo(this._container
, true);
865 WCF
.Message
.Submit
.registerButton('text', this._container
.find('.formSubmit button[data-type=save]'));
867 if (this._quoteManager
) {
868 // check if message field is empty
870 if ($.browser
.redactor
) {
871 if (this._messageField
.data('redactor')) {
872 this._editorCallback(this._messageField
.redactor('wutil.isEmptyEditor'));
876 $empty
= (!this._messageField
.val().length
);
877 this._editorCallback($empty
);
883 if (event
!== null) {
884 event
.stopPropagation();
890 * Inserts quotes and focuses the editor.
892 _editorCallback: function(isEmpty
) {
894 this._quoteManager
.insertQuotes(this._getClassName(), this._getObjectID(), $.proxy(this._insertQuotes
, this));
897 if ($.browser
.redactor
) {
898 this._messageField
.redactor('focus.setEnd');
901 this._messageField
.focus();
906 * Returns container element.
910 getContainer: function() {
911 return this._container
;
915 * Insertes quotes into the quick reply editor.
919 _insertQuotes: function(data
) {
920 if (!data
.returnValues
.template
) {
924 if ($.browser
.redactor
) {
925 var $html
= WCF
.String
.unescapeHTML(data
.returnValues
.template
);
926 $html
= this._messageField
.redactor('wbbcode.convertToHtml', $html
);
927 $html
= $html
.replace(/<p><blockquote/, '<blockquote');
928 $html
= $html
.replace(/blockquote><\/p>/, 'blockquote>');
930 this._messageField
.redactor('focus.setEnd');
931 this._messageField
.redactor('wutil.insertDynamic', $html
, data
.returnValues
.template
);
932 this._messageField
.redactor('wutil.selectionEndOfEditor');
933 this._messageField
.redactor('wbbcode._observeQuotes');
936 this._messageField
.val(data
.returnValues
.template
);
944 if (this._pendingSave
) {
949 if ($.browser
.redactor
) {
950 $message
= this._messageField
.redactor('wutil.getText');
953 $message
= $.trim(this._messageField
.val());
956 // check if message is empty
957 var $innerError
= this._messageField
.parent().find('small.innerError');
958 if ($message
=== '' || $message
=== '0') {
959 if (!$innerError
.length
) {
960 $innerError
= $('<small class="innerError" />').appendTo(this._messageField
.parent());
963 $innerError
.html(WCF
.Language
.get('wcf.global.form.error.empty'));
967 $innerError
.remove();
970 this._pendingSave
= true;
972 this._proxy
.setOption('data', {
973 actionName
: 'quickReply',
974 className
: this._getClassName(),
975 interfaceName
: 'wcf\\data\\IMessageQuickReplyAction',
976 parameters
: this._getParameters($message
)
978 this._proxy
.sendRequest();
980 // show spinner and hide Redactor
981 var $messageBody
= this._container
.find('.messageQuickReplyContent .messageBody');
982 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody
);
983 var $redactorBox
= $messageBody
.children('.redactor-box').hide();
986 $redactorBox
.next().hide();
989 $messageBody
.next().hide();
993 * Returns the parameters for the save request.
995 * @param string message
998 _getParameters: function(message
) {
1000 objectID
: this._getObjectID(),
1004 lastPostTime
: this._container
.data('lastPostTime'),
1005 pageNo
: this._container
.data('pageNo'),
1006 removeQuoteIDs
: (this._quoteManager
=== null ? [ ] : this._quoteManager
.getQuotesMarkedForRemoval())
1008 if (this._container
.data('anchor')) {
1009 $parameters
.anchor
= this._container
.data('anchor');
1012 WCF
.System
.Event
.fireEvent('com.woltlab.wcf.messageOptionsInline', 'submit_' + this._messageField
.wcfIdentify(), $parameters
.data
);
1018 * Cancels quick reply.
1020 _cancel: function() {
1021 this._revertQuickReply(true);
1023 if ($.browser
.redactor
) {
1024 this._messageField
.redactor('wutil.reset');
1027 this._messageField
.val('');
1032 * Reverts quick reply to original state and optionally hiding it.
1034 * @param boolean hide
1036 _revertQuickReply: function(hide
) {
1037 var $messageBody
= this._container
.find('.messageQuickReplyContent .messageBody');
1040 this._container
.hide();
1042 // remove previous error messages
1043 $messageBody
.children('small.innerError').remove();
1047 $messageBody
.children('.icon-spinner').remove();
1048 $messageBody
.children('.redactor-box').show().next().show();
1050 // display form submit
1051 $messageBody
.next().show();
1053 this._quickReplyButtons
.show();
1057 * Prepares jump to extended message add form.
1059 _prepareExtended: function() {
1060 this._pendingSave
= true;
1062 // mark quotes for removal
1063 if (this._quoteManager
!== null) {
1064 this._quoteManager
.markQuotesForRemoval();
1068 if ($.browser
.redactor
) {
1069 $message
= this._messageField
.redactor('wutil.getText');
1071 if ($message
.length
) {
1072 this._messageField
.redactor('wutil.saveTextToStorage', true);
1075 this._messageField
.redactor('wutil.autosavePurge');
1079 $message
= $.trim(this._messageField
.val());
1082 new WCF
.Action
.Proxy({
1085 actionName
: 'jumpToExtended',
1086 className
: this._getClassName(),
1087 interfaceName
: 'wcf\\data\\IExtendedMessageQuickReplyAction',
1089 containerID
: this._getObjectID(),
1093 success
: (function(data
) {
1094 this._messageField
.redactor('wutil.saveTextToStorage');
1095 window
.location
= data
.returnValues
.url
;
1101 * Handles successful AJAX calls.
1103 * @param object data
1104 * @param string textStatus
1105 * @param jQuery jqXHR
1107 _success: function(data
, textStatus
, jqXHR
) {
1108 if ($.browser
.redactor
) {
1109 this._messageField
.redactor('wutil.autosavePurge');
1112 // redirect to new page
1113 if (data
.returnValues
.url
) {
1114 window
.location
= data
.returnValues
.url
;
1117 if (data
.returnValues
.template
) {
1119 var $message
= $('' + data
.returnValues
.template
);
1120 if (this._container
.data('sortOrder') == 'DESC') {
1121 $message
.insertAfter(this._container
);
1124 $message
.insertBefore(this._container
);
1127 // update last post time
1128 this._container
.data('lastPostTime', data
.returnValues
.lastPostTime
);
1130 // show notification
1131 this._notification
.show(undefined, undefined, WCF
.Language
.get('wcf.global.success.add'));
1133 this._updateHistory($message
.wcfIdentify());
1136 // show notification
1137 var $message
= (this._successMessageNonVisible
) ? this._successMessageNonVisible
: 'wcf.global.success.add';
1138 this._notification
.show(undefined, 5000, WCF
.Language
.get($message
));
1141 if ($.browser
.redactor
) {
1142 this._messageField
.redactor('wutil.reset');
1145 this._messageField
.val('');
1148 // hide quick reply and revert it
1149 this._revertQuickReply(true);
1151 // count stored quotes
1152 if (this._quoteManager
!== null) {
1153 this._quoteManager
.countQuotes();
1156 this._pendingSave
= false;
1161 * Reverts quick reply on failure to preserve entered message.
1163 _failure: function(data
) {
1164 this._pendingSave
= false;
1165 this._revertQuickReply(false);
1167 if (data
=== null || data
.returnValues
=== undefined || data
.returnValues
.errorType
=== undefined) {
1171 var $messageBody
= this._container
.find('.messageQuickReplyContent .messageBody');
1172 var $innerError
= $messageBody
.children('small.innerError').empty();
1173 if (!$innerError
.length
) {
1174 $innerError
= $('<small class="innerError" />').appendTo($messageBody
);
1177 $innerError
.html(data
.returnValues
.errorType
);
1183 * Returns action class name.
1187 _getClassName: function() {
1192 * Returns object id.
1196 _getObjectID: function() {
1201 * Updates the history to avoid old content when going back in the browser
1206 _updateHistory: function(hash
) {
1207 window
.location
.hash
= hash
;
1212 * Provides an inline message editor.
1214 * @param integer containerID
1216 WCF
.Message
.InlineEditor
= Class
.extend({
1218 * currently active message
1221 _activeElementID
: '',
1242 * CSS selector for the message container
1245 _messageContainerSelector
: '.jsMessage',
1248 * prefix of the message editor CSS id
1251 _messageEditorIDPrefix
: 'messageEditor',
1254 * notification object
1255 * @var WCF.System.Notification
1257 _notification
: null,
1261 * @var WCF.Action.Proxy
1266 * quote manager object
1267 * @var WCF.Message.Quote.Manager
1269 _quoteManager
: null,
1272 * support for extended editing form
1275 _supportExtendedForm
: false,
1278 * Initializes a new WCF.Message.InlineEditor object.
1280 * @param integer containerID
1281 * @param boolean supportExtendedForm
1282 * @param WCF.Message.Quote.Manager quoteManager
1284 init: function(containerID
, supportExtendedForm
, quoteManager
) {
1285 this._activeElementID
= '';
1286 this._container
= { };
1287 this._containerID
= parseInt(containerID
);
1288 this._dropdowns
= { };
1289 this._quoteManager
= quoteManager
|| null;
1290 this._supportExtendedForm
= (supportExtendedForm
) ? true : false;
1291 this._proxy
= new WCF
.Action
.Proxy({
1292 failure
: $.proxy(this._failure
, this),
1293 showLoadingOverlay
: false,
1294 success
: $.proxy(this._success
, this)
1296 this._notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.global.success.edit'));
1298 this.initContainers();
1300 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.InlineEditor', $.proxy(this.initContainers
, this));
1304 * Initializes editing capability for all messages.
1306 initContainers: function() {
1307 $(this._messageContainerSelector
).each($.proxy(function(index
, container
) {
1308 var $container
= $(container
);
1309 var $containerID
= $container
.wcfIdentify();
1311 if (!this._container
[$containerID
]) {
1312 this._container
[$containerID
] = $container
;
1314 if ($container
.data('canEditInline')) {
1315 var $button
= $container
.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID
).click($.proxy(this._clickInline
, this));
1316 if ($container
.data('canEdit')) $button
.dblclick($.proxy(this._click
, this));
1318 else if ($container
.data('canEdit')) {
1319 $container
.find('.jsMessageEditButton:eq(0)').data('containerID', $containerID
).click($.proxy(this._click
, this));
1326 * Loads WYSIWYG editor for selected message.
1328 * @param object event
1329 * @param integer containerID
1332 _click: function(event
, containerID
) {
1333 var $containerID
= (event
=== null) ? containerID
: $(event
.currentTarget
).data('containerID');
1334 if (this._activeElementID
=== '') {
1335 this._activeElementID
= $containerID
;
1338 this._proxy
.setOption('data', {
1339 actionName
: 'beginEdit',
1340 className
: this._getClassName(),
1341 interfaceName
: 'wcf\\data\\IMessageInlineEditorAction',
1343 containerID
: this._containerID
,
1344 objectID
: this._container
[$containerID
].data('objectID')
1347 this._proxy
.setOption('failure', $.proxy(function() { this._cancel(); }, this));
1348 this._proxy
.sendRequest();
1351 var $notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.message.error.editorAlreadyInUse'), 'warning');
1352 $notification
.show();
1355 // force closing dropdown to avoid displaying the dropdown after
1357 if (this._dropdowns
[this._container
[$containerID
].data('objectID')]) {
1358 this._dropdowns
[this._container
[$containerID
].data('objectID')].removeClass('dropdownOpen');
1361 if (event
!== null) {
1362 event
.stopPropagation();
1368 * Provides an inline dropdown menu instead of directly loading the WYSIWYG editor.
1370 * @param object event
1373 _clickInline: function(event
) {
1374 var $button
= $(event
.currentTarget
);
1376 if (!$button
.hasClass('dropdownToggle')) {
1377 var $containerID
= $button
.data('containerID');
1379 $button
.addClass('dropdownToggle').parent().addClass('dropdown');
1381 var $dropdownMenu
= $('<ul class="dropdownMenu" />').insertAfter($button
);
1382 this._initDropdownMenu($containerID
, $dropdownMenu
);
1384 WCF
.DOMNodeInsertedHandler
.execute();
1386 this._dropdowns
[this._container
[$containerID
].data('objectID')] = $dropdownMenu
;
1388 WCF
.Dropdown
.registerCallback($button
.parent().wcfIdentify(), $.proxy(this._toggleDropdown
, this));
1390 // trigger click event
1391 $button
.trigger('click');
1394 event
.stopPropagation();
1399 * Handles errorneus editing requests.
1401 * @param object data
1403 _failure: function(data
) {
1404 this._revertEditor();
1406 if (data
=== null || data
.returnValues
=== undefined || data
.returnValues
.errorType
=== undefined) {
1410 var $messageBody
= this._container
[this._activeElementID
].find('.messageBody .messageInlineEditor');
1411 var $innerError
= $messageBody
.children('small.innerError').empty();
1412 if (!$innerError
.length
) {
1413 $innerError
= $('<small class="innerError" />').insertBefore($messageBody
.children('.formSubmit'));
1416 $innerError
.html(data
.returnValues
.errorType
);
1422 * Forces message options to stay visible if toggling dropdown menu.
1424 * @param string containerID
1425 * @param string action
1427 _toggleDropdown: function(containerID
, action
) {
1428 WCF
.Dropdown
.getDropdown(containerID
).parents('.messageOptions').toggleClass('forceOpen');
1432 * Initializes the inline edit dropdown menu.
1434 * @param integer containerID
1435 * @param jQuery dropdownMenu
1437 _initDropdownMenu: function(containerID
, dropdownMenu
) { },
1440 * Prepares message for WYSIWYG display.
1442 _prepare: function() {
1443 var $messageBody
= this._container
[this._activeElementID
].find('.messageBody');
1444 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody
);
1446 var $content
= $messageBody
.find('.messageText').hide();
1448 // hide unrelated content
1449 $content
.parent().children('.jsInlineEditorHideContent').hide();
1450 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').hide();
1454 * Cancels editing and reverts to original message.
1456 _cancel: function() {
1457 var $container
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget');
1460 this._destroyEditor();
1463 var $messageBody
= $container
.find('.messageBody');
1464 $messageBody
.children('.icon-spinner').remove();
1465 $messageBody
.find('.messageText').show();
1466 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').show();
1468 // show unrelated content
1469 $messageBody
.find('.jsInlineEditorHideContent').show();
1471 // revert message options
1472 this._container
[this._activeElementID
].find('.messageOptions').removeClass('forceHidden');
1474 this._activeElementID
= '';
1476 if (this._quoteManager
) {
1477 this._quoteManager
.clearAlternativeEditor();
1482 * Handles successful AJAX calls.
1484 * @param object data
1485 * @param string textStatus
1486 * @param jQuery jqXHR
1488 _success: function(data
, textStatus
, jqXHR
) {
1489 switch (data
.returnValues
.actionName
) {
1491 this._showEditor(data
);
1495 this._showMessage(data
);
1501 * Shows WYSIWYG editor for active message.
1503 * @param object data
1505 _showEditor: function(data
) {
1506 // revert failure function
1507 this._proxy
.setOption('failure', $.proxy(this._failure
, this));
1508 var $containerID
= this._messageEditorIDPrefix
+ this._container
[this._activeElementID
].data('objectID');
1510 var $messageBody
= this._container
[this._activeElementID
].addClass('jsInvalidQuoteTarget').find('.messageBody');
1511 $messageBody
.children('.icon-spinner').remove();
1512 var $content
= $messageBody
.children('div:eq(0)');
1515 $('' + data
.returnValues
.template
).appendTo($content
);
1518 var $formSubmit
= $content
.find('.formSubmit');
1519 var $saveButton
= $formSubmit
.find('button[data-type=save]').click($.proxy(this._save
, this));
1520 if (this._supportExtendedForm
) $formSubmit
.find('button[data-type=extended]').click($.proxy(this._prepareExtended
, this));
1521 $formSubmit
.find('button[data-type=cancel]').click($.proxy(this._cancel
, this));
1523 // TODO: is this still used?
1524 WCF
.Message
.Submit
.registerButton(
1525 this._messageEditorIDPrefix
+ this._container
[this._activeElementID
].data('objectID'),
1529 WCF
.System
.Event
.addListener('com.woltlab.wcf.redactor', 'submitEditor_' + $containerID
, function(data
) {
1532 $saveButton
.trigger('click');
1535 // hide message options
1536 this._container
[this._activeElementID
].find('.messageOptions').addClass('forceHidden');
1538 var $element
= $('#' + $containerID
);
1539 if ($.browser
.redactor
) {
1540 new WCF
.PeriodicalExecuter($.proxy(function(pe
) {
1543 if (this._quoteManager
) {
1544 this._quoteManager
.setAlternativeEditor($element
);
1547 new WCF
.Effect
.Scroll().scrollTo(this._container
[this._activeElementID
], true);
1558 _revertEditor: function() {
1559 var $messageBody
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget').find('.messageBody');
1560 $messageBody
.children('span.icon-spinner').remove();
1561 $messageBody
.children('div:eq(0)').children(':not(.messageText)').show();
1562 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').show();
1564 // show unrelated content
1565 $messageBody
.find('.jsInlineEditorHideContent').show();
1567 if (this._quoteManager
) {
1568 this._quoteManager
.clearAlternativeEditor();
1573 * Saves editor contents.
1576 var $container
= this._container
[this._activeElementID
];
1577 var $objectID
= $container
.data('objectID');
1580 if ($.browser
.redactor
) {
1581 $message
= $('#' + this._messageEditorIDPrefix
+ $objectID
).redactor('wutil.getText');
1584 $message
= $('#' + this._messageEditorIDPrefix
+ $objectID
).val();
1588 containerID
: this._containerID
,
1595 WCF
.System
.Event
.fireEvent('com.woltlab.wcf.messageOptionsInline', 'submit_' + this._messageEditorIDPrefix
+ $objectID
, $parameters
);
1597 this._proxy
.setOption('data', {
1599 className
: this._getClassName(),
1600 interfaceName
: 'wcf\\data\\IMessageInlineEditorAction',
1601 parameters
: $parameters
1603 this._proxy
.sendRequest();
1609 * Prepares jumping to extended editing mode.
1611 _prepareExtended: function() {
1612 var $container
= this._container
[this._activeElementID
];
1613 var $objectID
= $container
.data('objectID');
1616 if ($.browser
.redactor
) {
1617 $message
= $('#' + this._messageEditorIDPrefix
+ $objectID
).redactor('wutil.getText');
1620 $message
= $('#' + this._messageEditorIDPrefix
+ $objectID
).val();
1623 new WCF
.Action
.Proxy({
1626 actionName
: 'jumpToExtended',
1627 className
: this._getClassName(),
1629 containerID
: this._containerID
,
1631 messageID
: $objectID
1634 success: function(data
, textStatus
, jqXHR
) {
1635 window
.location
= data
.returnValues
.url
;
1641 * Hides WYSIWYG editor.
1643 _hideEditor: function() {
1644 var $messageBody
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget').find('.messageBody');
1645 $('<span class="icon icon48 icon-spinner" />').appendTo($messageBody
);
1646 $messageBody
.children('div:eq(0)').children().hide();
1647 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').show();
1649 // show unrelated content
1650 $messageBody
.find('.jsInlineEditorHideContent').show();
1652 if (this._quoteManager
) {
1653 this._quoteManager
.clearAlternativeEditor();
1658 * Shows rendered message.
1660 * @param object data
1662 _showMessage: function(data
) {
1663 var $container
= this._container
[this._activeElementID
].removeClass('jsInvalidQuoteTarget');
1664 var $messageBody
= $container
.find('.messageBody');
1665 $messageBody
.children('.icon-spinner').remove();
1666 var $content
= $messageBody
.children('div:eq(0)');
1668 // show unrelated content
1669 $content
.parent().children('.jsInlineEditorHideContent').show();
1671 // revert message options
1672 this._container
[this._activeElementID
].find('.messageOptions').removeClass('forceHidden');
1675 this._destroyEditor();
1677 $content
.children('.messageText').html(data
.returnValues
.message
).show();
1679 if (data
.returnValues
.attachmentList
== undefined) {
1680 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').show();
1683 $messageBody
.children('.attachmentThumbnailList, .attachmentFileList').remove();
1685 if (data
.returnValues
.attachmentList
) {
1686 $(data
.returnValues
.attachmentList
).insertAfter($messageBody
.children('div:eq(0)'));
1690 this._activeElementID
= '';
1692 this._updateHistory(this._getHash($container
.data('objectID')));
1694 this._notification
.show();
1696 if (this._quoteManager
) {
1697 this._quoteManager
.clearAlternativeEditor();
1702 * Destroies editor instance and removes it's DOM elements.
1704 _destroyEditor: function() {
1705 var $container
= this._container
[this._activeElementID
];
1708 if ($.browser
.redactor
) {
1709 var $target
= $('#' + this._messageEditorIDPrefix
+ $container
.data('objectID'));
1710 $target
.redactor('wutil.autosavePurge');
1711 $target
.redactor('core.destroy');
1714 // purge DOM elements
1715 $container
.find('.messageBody > div > .messageInlineEditor').remove();
1717 // remove event listeners
1718 WCF
.System
.Event
.removeAllListeners('com.woltlab.wcf.messageOptionsInline', 'submit_' + this._messageEditorIDPrefix
+ $container
.data('objectID'));
1722 * Returns message action class name.
1726 _getClassName: function() {
1731 * Returns the hash added to the url after successfully editing a message.
1735 _getHash: function(objectID
) {
1736 return '#message' + objectID
;
1740 * Updates the history to avoid old content when going back in the browser
1745 _updateHistory: function(hash
) {
1746 window
.location
.hash
= hash
;
1751 * Handles submit buttons for forms with an embedded WYSIWYG editor.
1753 WCF
.Message
.Submit
= {
1755 * list of registered buttons
1761 * Registers submit button for specified wysiwyg container id.
1763 * @param string wysiwygContainerID
1764 * @param string selector
1766 registerButton: function(wysiwygContainerID
, selector
) {
1767 if (!WCF
.Browser
.isChrome()) {
1771 this._buttons
[wysiwygContainerID
] = $(selector
);
1775 * Triggers 'click' event for registered buttons.
1777 execute: function(wysiwygContainerID
) {
1778 if (!this._buttons
[wysiwygContainerID
]) {
1782 this._buttons
[wysiwygContainerID
].trigger('click');
1787 * Namespace for message quotes.
1789 WCF
.Message
.Quote
= { };
1792 * Handles message quotes.
1794 * @param string className
1795 * @param string objectType
1796 * @param string containerSelector
1797 * @param string messageBodySelector
1799 WCF
.Message
.Quote
.Handler
= Class
.extend({
1801 * active container id
1804 _activeContainerID
: '',
1813 * list of message containers
1819 * container selector
1822 _containerSelector
: '',
1825 * 'copy quote' overlay
1837 * message body selector
1840 _messageBodySelector
: '',
1856 * @var WCF.Action.Proxy
1862 * @var WCF.Message.Quote.Manager
1864 _quoteManager
: null,
1867 * Initializes the quote handler for given object type.
1869 * @param WCF.Message.Quote.Manager quoteManager
1870 * @param string className
1871 * @param string objectType
1872 * @param string containerSelector
1873 * @param string messageBodySelector
1874 * @param string messageContentSelector
1875 * @param boolean supportDirectInsert
1877 init: function(quoteManager
, className
, objectType
, containerSelector
, messageBodySelector
, messageContentSelector
, supportDirectInsert
) {
1878 this._className
= className
;
1879 if (this._className
== '') {
1880 console
.debug("[WCF.Message.QuoteManager] Empty class name given, aborting.");
1884 this._objectType
= objectType
;
1885 if (this._objectType
== '') {
1886 console
.debug("[WCF.Message.QuoteManager] Empty object type name given, aborting.");
1890 this._containerSelector
= containerSelector
;
1892 this._messageBodySelector
= messageBodySelector
;
1893 this._messageContentSelector
= messageContentSelector
;
1895 this._proxy
= new WCF
.Action
.Proxy({
1896 success
: $.proxy(this._success
, this)
1899 this._initContainers();
1901 supportDirectInsert
= (supportDirectInsert
&& quoteManager
.supportPaste()) ? true : false;
1902 this._initCopyQuote(supportDirectInsert
);
1904 $(document
).mouseup($.proxy(this._mouseUp
, this));
1906 // register with quote manager
1907 this._quoteManager
= quoteManager
;
1908 this._quoteManager
.register(this._objectType
, this);
1910 // register with DOMNodeInsertedHandler
1911 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.Quote.Handler' + objectType
.hashCode(), $.proxy(this._initContainers
, this));
1915 * Initializes message containers.
1917 _initContainers: function() {
1919 $(this._containerSelector
).each(function(index
, container
) {
1920 var $container
= $(container
);
1921 var $containerID
= $container
.wcfIdentify();
1923 if (!self
._containers
[$containerID
]) {
1924 self
._containers
[$containerID
] = $container
;
1925 if ($container
.hasClass('jsInvalidQuoteTarget')) {
1929 if (self
._messageBodySelector
!== null) {
1930 $container
= $container
.find(self
._messageBodySelector
).data('containerID', $containerID
);
1933 $container
.mousedown($.proxy(self
._mouseDown
, self
));
1935 // bind event to quote whole message
1936 self
._containers
[$containerID
].find('.jsQuoteMessage').click($.proxy(self
._saveFullQuote
, self
));
1942 * Handles mouse down event.
1944 * @param object event
1946 _mouseDown: function(event
) {
1948 this._copyQuote
.hide();
1950 // store container ID
1951 var $container
= $(event
.currentTarget
);
1953 if (this._messageBodySelector
) {
1954 $container
= this._containers
[$container
.data('containerID')];
1957 if ($container
.hasClass('jsInvalidQuoteTarget')) {
1958 this._activeContainerID
= '';
1963 this._activeContainerID
= $container
.wcfIdentify();
1965 // remove alt-tag from all images, fixes quoting in Firefox
1966 if ($.browser
.mozilla
) {
1967 // TODO: is this still required?
1968 $container
.find('img').each(function() {
1969 var $image
= $(this);
1970 $image
.data('__alt', $image
.attr('alt')).removeAttr('alt');
1976 * Returns the text of a node and its children.
1978 * @param object node
1981 _getNodeText: function(node
) {
1982 // work-around for IE, see http://stackoverflow.com/a/5983176
1983 var $nodeFilter = function(node
) {
1984 if (node
.tagName
=== 'H3') {
1985 return NodeFilter
.FILTER_REJECT
;
1988 return NodeFilter
.FILTER_ACCEPT
;
1990 $nodeFilter
.acceptNode
= $nodeFilter
;
1992 var $walker
= document
.createTreeWalker(
1994 NodeFilter
.SHOW_ELEMENT
| NodeFilter
.SHOW_TEXT
,
2000 while ($walker
.nextNode()) {
2001 var $node
= $walker
.currentNode
;
2003 if ($node
.nodeType
=== Node
.ELEMENT_NODE
) {
2004 switch ($node
.tagName
) {
2012 if (!$.browser
.msie
) {
2019 $text
+= $walker
.currentNode
.nodeValue
;
2028 * Handles the mouse up event.
2030 * @param object event
2032 _mouseUp: function(event
) {
2034 if (this._activeContainerID
== '') {
2035 this._copyQuote
.hide();
2040 var $container
= this._containers
[this._activeContainerID
];
2041 var $selection
= this._getSelectedText();
2042 var $text
= $.trim($selection
);
2044 this._copyQuote
.hide();
2049 // compare selection with message text of given container
2050 var $messageText
= null;
2051 if (this._messageBodySelector
) {
2052 $messageText
= this._getNodeText($container
.find(this._messageContentSelector
)[0]);
2055 $messageText
= this._getNodeText($container
[0]);
2058 // selected text is not part of $messageText or contains text from unrelated nodes
2059 if (this._normalize($messageText
).indexOf(this._normalize($text
)) === -1) {
2062 this._copyQuote
.show();
2064 var $coordinates
= this._getBoundingRectangle($container
, window
.getSelection());
2065 var $dimensions
= this._copyQuote
.getDimensions('outer');
2066 var $left
= ($coordinates
.right
- $coordinates
.left
) / 2 - ($dimensions
.width
/ 2) + $coordinates
.left
;
2068 this._copyQuote
.css({
2069 top
: $coordinates
.top
- $dimensions
.height
- 7 + 'px',
2072 this._copyQuote
.hide();
2074 // reset containerID
2075 this._activeContainerID
= '';
2077 // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
2079 new WCF
.PeriodicalExecuter(function(pe
) {
2082 var $text
= $.trim(self
._getSelectedText());
2084 self
._copyQuote
.show();
2085 self
._message
= $text
;
2086 self
._objectID
= $container
.data('objectID');
2088 // revert alt tags, fixes quoting in Firefox
2089 if ($.browser
.mozilla
) {
2090 // TODO: is this still required?
2091 $container
.find('img').each(function() {
2092 var $image
= $(this);
2093 $image
.attr('alt', $image
.data('__alt'));
2101 * Normalizes a text for comparison.
2103 * @param string text
2106 _normalize: function(text
) {
2107 return text
.replace(/\r?\n|\r/g, "\n").replace(/\s/g, ' ').replace(/\s{2,}/g, ' ');
2111 * Returns the left or right offset of the current text selection.
2113 * @param object range
2114 * @param boolean before
2117 _getOffset: function(range
, before
) {
2118 range
.collapse(before
);
2120 var $elementID
= WCF
.getRandomID();
2121 var $element
= document
.createElement('span');
2122 $element
.innerHTML
= '<span id="' + $elementID
+ '"></span>';
2123 var $fragment
= document
.createDocumentFragment(), $node
;
2124 while ($node
= $element
.firstChild
) {
2125 $fragment
.appendChild($node
);
2127 range
.insertNode($fragment
);
2129 $element
= $('#' + $elementID
);
2130 var $position
= $element
.offset();
2131 $position
.top
= $position
.top
- $(window
).scrollTop();
2138 * Returns the offsets of the selection's bounding rectangle.
2142 _getBoundingRectangle: function(container
, selection
) {
2143 var $coordinates
= null;
2145 if (document
.createRange
&& typeof document
.createRange().getBoundingClientRect
!= "undefined") { // Opera, Firefox, Safari, Chrome
2146 if (selection
.rangeCount
> 0) {
2147 // the coordinates returned by getBoundingClientRect() is relative to the window, not the document!
2148 var $rect
= selection
.getRangeAt(0).getBoundingClientRect();
2153 top
: $rect
.top
+ $(document
).scrollTop()
2157 else if (document
.selection
&& document
.selection
.type
!= "Control") { // IE
2158 var $range
= document
.selection
.createRange();
2161 left
: $range
.boundingLeft
,
2162 right
: $range
.boundingRight
,
2163 top
: $range
.boundingTop
2167 return $coordinates
;
2171 * Saves current selection.
2173 * @see http://stackoverflow.com/a/13950376
2175 * @param object containerEl
2178 _saveSelection: function(containerEl
) {
2179 if (window
.getSelection
&& document
.createRange
) {
2180 var range
= window
.getSelection().getRangeAt(0);
2181 var preSelectionRange
= range
.cloneRange();
2182 preSelectionRange
.selectNodeContents(containerEl
);
2183 preSelectionRange
.setEnd(range
.startContainer
, range
.startOffset
);
2184 var start
= preSelectionRange
.toString().length
;
2188 end
: start
+ range
.toString().length
2192 var selectedTextRange
= document
.selection
.createRange();
2193 var preSelectionTextRange
= document
.body
.createTextRange();
2194 preSelectionTextRange
.moveToElementText(containerEl
);
2195 preSelectionTextRange
.setEndPoint("EndToStart", selectedTextRange
);
2196 var start
= preSelectionTextRange
.text
.length
;
2200 end
: start
+ selectedTextRange
.text
.length
2206 * Restores a selection.
2208 * @see http://stackoverflow.com/a/13950376
2210 * @param object containerEl
2211 * @param object savedSel
2213 _restoreSelection: function(containerEl
, savedSel
) {
2214 if (window
.getSelection
&& document
.createRange
) {
2215 var charIndex
= 0, range
= document
.createRange();
2216 range
.setStart(containerEl
, 0);
2217 range
.collapse(true);
2218 var nodeStack
= [containerEl
], node
, foundStart
= false, stop
= false;
2220 while (!stop
&& (node
= nodeStack
.pop())) {
2221 if (node
.nodeType
== Node
.TEXT_NODE
) {
2222 var nextCharIndex
= charIndex
+ node
.length
;
2223 if (!foundStart
&& savedSel
.start
>= charIndex
&& savedSel
.start
<= nextCharIndex
) {
2224 range
.setStart(node
, savedSel
.start
- charIndex
);
2227 if (foundStart
&& savedSel
.end
>= charIndex
&& savedSel
.end
<= nextCharIndex
) {
2228 range
.setEnd(node
, savedSel
.end
- charIndex
);
2231 charIndex
= nextCharIndex
;
2233 var i
= node
.childNodes
.length
;
2235 nodeStack
.push(node
.childNodes
[i
]);
2240 var sel
= window
.getSelection();
2241 sel
.removeAllRanges();
2242 sel
.addRange(range
);
2245 var textRange
= document
.body
.createTextRange();
2246 textRange
.moveToElementText(containerEl
);
2247 textRange
.collapse(true);
2248 textRange
.moveEnd("character", savedSel
.end
);
2249 textRange
.moveStart("character", savedSel
.start
);
2255 * Initializes the 'copy quote' element.
2257 * @param boolean supportDirectInsert
2259 _initCopyQuote: function(supportDirectInsert
) {
2260 this._copyQuote
= $('#quoteManagerCopy');
2261 if (!this._copyQuote
.length
) {
2262 this._copyQuote
= $('<div id="quoteManagerCopy" class="balloonTooltip"><span class="jsQuoteManagerStore">' + WCF
.Language
.get('wcf.message.quote.quoteSelected') + '</span><span class="pointer"><span></span></span></div>').hide().appendTo(document
.body
);
2263 var $storeQuote
= this._copyQuote
.children('span.jsQuoteManagerStore').click($.proxy(this._saveQuote
, this));
2264 if (supportDirectInsert
) {
2265 $('<span class="jsQuoteManagerQuoteAndInsert">' + WCF
.Language
.get('wcf.message.quote.quoteAndReply') + '</span>').click($.proxy(this._saveAndInsertQuote
, this)).insertAfter($storeQuote
);
2271 * Returns the text selection.
2275 _getSelectedText: function() {
2276 var $selection
= window
.getSelection();
2277 if ($selection
.rangeCount
) {
2278 return this._getNodeText($selection
.getRangeAt(0).cloneContents());
2285 * Saves a full quote.
2287 * @param object event
2289 _saveFullQuote: function(event
) {
2290 var $listItem
= $(event
.currentTarget
);
2292 this._proxy
.setOption('data', {
2293 actionName
: 'saveFullQuote',
2294 className
: this._className
,
2295 interfaceName
: 'wcf\\data\\IMessageQuoteAction',
2296 objectIDs
: [ $listItem
.data('objectID') ]
2298 this._proxy
.sendRequest();
2300 // mark element as quoted
2301 if ($listItem
.data('isQuoted')) {
2302 $listItem
.data('isQuoted', false).children('a').removeClass('active');
2305 $listItem
.data('isQuoted', true).children('a').addClass('active');
2308 // close navigation on mobile
2309 var $navigationList
= $listItem
.parents('.buttonGroupNavigation');
2310 if ($navigationList
.hasClass('jsMobileButtonGroupNavigation')) {
2311 $navigationList
.children('.dropdownLabel').trigger('click');
2315 event
.stopPropagation();
2322 * @param boolean renderQuote
2324 _saveQuote: function(renderQuote
) {
2325 renderQuote
= (renderQuote
=== true) ? true : false;
2327 this._proxy
.setOption('data', {
2328 actionName
: 'saveQuote',
2329 className
: this._className
,
2330 interfaceName
: 'wcf\\data\\IMessageQuoteAction',
2331 objectIDs
: [ this._objectID
],
2333 message
: this._message
,
2334 renderQuote
: renderQuote
2337 this._proxy
.sendRequest();
2341 * Saves a quote and directly inserts it.
2343 _saveAndInsertQuote: function() {
2344 this._saveQuote(true);
2348 * Handles successful AJAX requests.
2350 * @param object data
2351 * @param string textStatus
2352 * @param jQuery jqXHR
2354 _success: function(data
, textStatus
, jqXHR
) {
2355 if (data
.returnValues
.count
!== undefined) {
2356 if (data
.returnValues
.fullQuoteMessageIDs
!== undefined) {
2357 data
.returnValues
.fullQuoteObjectIDs
= data
.returnValues
.fullQuoteMessageIDs
;
2360 var $fullQuoteObjectIDs
= (data
.returnValues
.fullQuoteObjectIDs
!== undefined) ? data
.returnValues
.fullQuoteObjectIDs
: { };
2361 this._quoteManager
.updateCount(data
.returnValues
.count
, $fullQuoteObjectIDs
);
2364 switch (data
.actionName
) {
2366 case 'saveFullQuote':
2367 if (data
.returnValues
.renderedQuote
) {
2368 WCF
.System
.Event
.fireEvent('com.woltlab.wcf.message.quote', 'insert', {
2369 forceInsert
: (data
.actionName
=== 'saveQuote' ? true : false),
2370 quote
: data
.returnValues
.renderedQuote
2378 * Updates the full quote data for all matching objects.
2380 * @param array<integer> $objectIDs
2382 updateFullQuoteObjectIDs: function(objectIDs
) {
2383 for (var $containerID
in this._containers
) {
2384 this._containers
[$containerID
].find('.jsQuoteMessage').each(function(index
, button
) {
2385 // reset all markings
2386 var $button
= $(button
).data('isQuoted', 0);
2387 $button
.children('a').removeClass('active');
2390 if (WCF
.inArray($button
.data('objectID'), objectIDs
)) {
2391 $button
.data('isQuoted', 1).children('a').addClass('active');
2399 * Manages stored quotes.
2401 * @param integer count
2403 WCF
.Message
.Quote
.Manager
= Class
.extend({
2405 * list of form buttons
2411 * number of stored quotes
2426 _editorElement
: null,
2429 * alternative Redactor element
2432 _editorElementAlternative
: null,
2441 * list of quote handlers
2447 * true, if an up-to-date template exists
2450 _hasTemplate
: false,
2453 * true, if related quotes should be inserted
2456 _insertQuotes
: true,
2460 * @var WCF.Action.Proxy
2465 * list of quotes to remove upon submit
2466 * @var array<string>
2468 _removeOnSubmit
: [ ],
2471 * show quotes element
2480 _supportPaste
: false,
2483 * Initializes the quote manager.
2485 * @param integer count
2486 * @param string elementID
2487 * @param boolean supportPaste
2488 * @param array<string> removeOnSubmit
2490 init: function(count
, elementID
, supportPaste
, removeOnSubmit
) {
2495 this._count
= parseInt(count
) || 0;
2496 this._dialog
= null;
2497 this._editorElement
= null;
2498 this._editorElementAlternative
= null;
2500 this._handlers
= { };
2501 this._hasTemplate
= false;
2502 this._insertQuotes
= true;
2503 this._removeOnSubmit
= [ ];
2504 this._showQuotes
= null;
2505 this._supportPaste
= false;
2508 this._editorElement
= $('#' + elementID
);
2509 if (this._editorElement
.length
) {
2510 this._supportPaste
= true;
2512 // get surrounding form-tag
2513 this._form
= this._editorElement
.parents('form:eq(0)');
2514 if (this._form
.length
) {
2515 this._form
.submit($.proxy(this._submit
, this));
2516 this._removeOnSubmit
= removeOnSubmit
|| [ ];
2522 this._supportPaste
= (supportPaste
=== true) ? true : false;
2527 this._proxy
= new WCF
.Action
.Proxy({
2528 showLoadingOverlay
: false,
2529 success
: $.proxy(this._success
, this),
2530 url
: 'index.php/MessageQuote/?t=' + SECURITY_TOKEN
+ SID_ARG_2ND
2533 this._toggleShowQuotes();
2537 * Sets an alternative editor element on runtime.
2539 * @param jQuery element
2541 setAlternativeEditor: function(element
) {
2542 this._editorElementAlternative
= element
;
2546 * Clears alternative editor element.
2548 clearAlternativeEditor: function() {
2549 this._editorElementAlternative
= null;
2553 * Registers a quote handler.
2555 * @param string objectType
2556 * @param WCF.Message.Quote.Handler handler
2558 register: function(objectType
, handler
) {
2559 this._handlers
[objectType
] = handler
;
2563 * Updates number of stored quotes.
2565 * @param integer count
2566 * @param object fullQuoteObjectIDs
2568 updateCount: function(count
, fullQuoteObjectIDs
) {
2569 this._count
= parseInt(count
) || 0;
2571 this._toggleShowQuotes();
2573 // update full quote ids of handlers
2574 for (var $objectType
in this._handlers
) {
2575 var $objectIDs
= fullQuoteObjectIDs
[$objectType
] || [ ];
2576 this._handlers
[$objectType
].updateFullQuoteObjectIDs($objectIDs
);
2581 * Inserts all associated quotes upon first time using quick reply.
2583 * @param string className
2584 * @param integer parentObjectID
2585 * @param object callback
2587 insertQuotes: function(className
, parentObjectID
, callback
) {
2588 if (!this._insertQuotes
) {
2589 this._insertQuotes
= true;
2594 new WCF
.Action
.Proxy({
2597 actionName
: 'getRenderedQuotes',
2598 className
: className
,
2599 interfaceName
: 'wcf\\data\\IMessageQuoteAction',
2601 parentObjectID
: parentObjectID
2609 * Toggles the display of the 'Show quotes' button
2611 _toggleShowQuotes: function() {
2613 if (this._showQuotes
!== null) {
2614 this._showQuotes
.hide();
2618 if (this._showQuotes
=== null) {
2619 this._showQuotes
= $('#showQuotes');
2620 if (!this._showQuotes
.length
) {
2621 this._showQuotes
= $('<div id="showQuotes" class="balloonTooltip" />').click($.proxy(this._click
, this)).appendTo(document
.body
);
2625 var $text
= WCF
.Language
.get('wcf.message.quote.showQuotes').replace(/#count#/, this._count
);
2626 this._showQuotes
.text($text
).show();
2629 this._hasTemplate
= false;
2633 * Handles clicks on 'Show quotes'.
2635 _click: function() {
2636 if (this._hasTemplate
) {
2637 this._dialog
.wcfDialog('open');
2640 this._proxy
.showLoadingOverlayOnce();
2642 this._proxy
.setOption('data', {
2643 actionName
: 'getQuotes',
2644 supportPaste
: this._supportPaste
2646 this._proxy
.sendRequest();
2651 * Renders the dialog.
2653 * @param string template
2655 renderDialog: function(template
) {
2656 // create dialog if not exists
2657 if (this._dialog
=== null) {
2658 this._dialog
= $('#messageQuoteList');
2659 if (!this._dialog
.length
) {
2660 this._dialog
= $('<div id="messageQuoteList" />').hide().appendTo(document
.body
);
2665 this._dialog
.html(template
);
2667 // add 'insert' and 'delete' buttons
2668 var $formSubmit
= $('<div class="formSubmit" />').appendTo(this._dialog
);
2669 if (this._supportPaste
) this._buttons
.insert
= $('<button class="buttonPrimary">' + WCF
.Language
.get('wcf.message.quote.insertAllQuotes') + '</button>').click($.proxy(this._insertSelected
, this)).appendTo($formSubmit
);
2670 this._buttons
.remove
= $('<button>' + WCF
.Language
.get('wcf.message.quote.removeAllQuotes') + '</button>').click($.proxy(this._removeSelected
, this)).appendTo($formSubmit
);
2673 this._dialog
.wcfDialog({
2674 title
: WCF
.Language
.get('wcf.message.quote.manageQuotes')
2676 this._dialog
.wcfDialog('render');
2677 this._hasTemplate
= true;
2679 // bind event listener
2680 var $insertQuoteButtons
= this._dialog
.find('.jsInsertQuote');
2681 if (this._supportPaste
) {
2682 $insertQuoteButtons
.click($.proxy(this._insertQuote
, this));
2685 $insertQuoteButtons
.hide();
2688 this._dialog
.find('input.jsCheckbox').change($.proxy(this._changeButtons
, this));
2690 // mark quotes for removal
2691 if (this._removeOnSubmit
.length
) {
2693 this._dialog
.find('input.jsRemoveQuote').each(function(index
, input
) {
2694 var $input
= $(input
).change($.proxy(this._change
, this));
2696 // mark for deletion
2697 if (WCF
.inArray($input
.parent('li').attr('data-quote-id'), self
._removeOnSubmit
)) {
2698 $input
.attr('checked', 'checked');
2705 * Updates button labels if a checkbox is checked or unchecked.
2707 _changeButtons: function() {
2709 if (this._dialog
.find('input.jsCheckbox:checked').length
) {
2710 if (this._supportPaste
) this._buttons
.insert
.html(WCF
.Language
.get('wcf.message.quote.insertSelectedQuotes'));
2711 this._buttons
.remove
.html(WCF
.Language
.get('wcf.message.quote.removeSelectedQuotes'));
2714 // no selection, pick all
2715 if (this._supportPaste
) this._buttons
.insert
.html(WCF
.Language
.get('wcf.message.quote.insertAllQuotes'));
2716 this._buttons
.remove
.html(WCF
.Language
.get('wcf.message.quote.removeAllQuotes'));
2721 * Checks for change event on delete-checkboxes.
2723 * @param object event
2725 _change: function(event
) {
2726 var $input
= $(event
.currentTarget
);
2727 var $quoteID
= $input
.parent('li').attr('data-quote-id');
2729 if ($input
.prop('checked')) {
2730 this._removeOnSubmit
.push($quoteID
);
2733 for (var $index
in this._removeOnSubmit
) {
2734 if (this._removeOnSubmit
[$index
] == $quoteID
) {
2735 delete this._removeOnSubmit
[$index
];
2743 * Inserts the selected quotes.
2745 _insertSelected: function() {
2746 if (this._editorElementAlternative
=== null) {
2747 var $api
= $('.jsQuickReply:eq(0)').data('__api');
2748 if ($api
&& !$api
.getContainer().is(':visible')) {
2749 this._insertQuotes
= false;
2754 if (!this._dialog
.find('input.jsCheckbox:checked').length
) {
2755 this._dialog
.find('input.jsCheckbox').prop('checked', 'checked');
2758 // insert all quotes
2759 this._dialog
.find('input.jsCheckbox:checked').each($.proxy(function(index
, input
) {
2760 this._insertQuote(null, input
);
2764 this._dialog
.wcfDialog('close');
2770 * @param object event
2771 * @param object inputElement
2773 _insertQuote: function(event
, inputElement
) {
2774 var $listItem
= (event
=== null) ? $(inputElement
).parents('li') : $(event
.currentTarget
).parents('li');
2775 var $quote
= $.trim($listItem
.children('div.jsFullQuote').text());
2776 var $message
= $listItem
.parents('article.message');
2778 // insert into editor
2779 if ($.browser
.redactor
) {
2780 if (this._editorElementAlternative
=== null) {
2781 this._editorElement
.redactor('wbbcode.insertQuoteBBCode', $message
.attr('data-username'), $message
.data('link'), $quote
, $quote
);
2784 this._editorElementAlternative
.redactor('wbbcode.insertQuoteBBCode', $message
.attr('data-username'), $message
.data('link'), $quote
, $quote
);
2789 $quote
= "[quote='" + $message
.attr('data-username') + "','" + $message
.data('link') + "']" + $quote
+ "[/quote]";
2792 var $textarea
= (this._editorElementAlternative
=== null) ? this._editorElement
: this._editorElementAlternative
;
2793 var $value
= $textarea
.val();
2795 if ($value
.length
== 0) {
2796 $textarea
.val($quote
);
2799 var $position
= $textarea
.getCaret();
2800 $textarea
.val( $value
.substr(0, $position
) + $quote
+ $value
.substr($position
) );
2804 // remove quote upon submit or upon request
2805 this._removeOnSubmit
.push($listItem
.attr('data-quote-id'));
2808 if (event
!== null) {
2809 this._dialog
.wcfDialog('close');
2812 if (event
!== null && this._editorElementAlternative
=== null) {
2813 var $api
= $('.jsQuickReply:eq(0)').data('__api');
2814 if ($api
&& !$api
.getContainer().is(':visible')) {
2815 this._insertQuotes
= false;
2822 * Removes selected quotes.
2824 _removeSelected: function() {
2825 if (!this._dialog
.find('input.jsCheckbox:checked').length
) {
2826 this._dialog
.find('input.jsCheckbox').prop('checked', 'checked');
2829 var $quoteIDs
= [ ];
2830 this._dialog
.find('input.jsCheckbox:checked').each(function(index
, input
) {
2831 $quoteIDs
.push($(input
).parents('li').attr('data-quote-id'));
2834 if ($quoteIDs
.length
) {
2836 var $objectTypes
= [ ];
2837 for (var $objectType
in this._handlers
) {
2838 $objectTypes
.push($objectType
);
2841 this._proxy
.setOption('data', {
2842 actionName
: 'remove',
2843 getFullQuoteObjectIDs
: this._handlers
.length
> 0,
2844 objectTypes
: $objectTypes
,
2847 this._proxy
.sendRequest();
2849 this._dialog
.wcfDialog('close');
2854 * Appends list of quote ids to remove after successful submit.
2856 _submit: function() {
2857 if (this._supportPaste
&& this._removeOnSubmit
.length
> 0) {
2858 var $formSubmit
= this._form
.find('.formSubmit');
2859 for (var $i
in this._removeOnSubmit
) {
2860 $('<input type="hidden" name="__removeQuoteIDs[]" value="' + this._removeOnSubmit
[$i
] + '" />').appendTo($formSubmit
);
2866 * Returns a list of quote ids marked for removal.
2868 * @return array<integer>
2870 getQuotesMarkedForRemoval: function() {
2871 return this._removeOnSubmit
;
2875 * Marks quote ids for removal.
2877 markQuotesForRemoval: function() {
2878 if (this._removeOnSubmit
.length
) {
2879 this._proxy
.setOption('data', {
2880 actionName
: 'markForRemoval',
2881 quoteIDs
: this._removeOnSubmit
2883 this._proxy
.suppressErrors();
2884 this._proxy
.sendRequest();
2889 * Removes all marked quote ids.
2891 removeMarkedQuotes: function() {
2892 if (this._removeOnSubmit
.length
) {
2893 this._proxy
.setOption('data', {
2894 actionName
: 'removeMarkedQuotes',
2895 getFullQuoteObjectIDs
: this._handlers
.length
> 0
2897 this._proxy
.sendRequest();
2902 * Counts stored quotes.
2904 countQuotes: function() {
2905 var $objectTypes
= [ ];
2906 for (var $objectType
in this._handlers
) {
2907 $objectTypes
.push($objectType
);
2910 this._proxy
.setOption('data', {
2911 actionName
: 'count',
2912 getFullQuoteObjectIDs
: ($objectTypes
.length
> 0),
2913 objectTypes
: $objectTypes
2915 this._proxy
.sendRequest();
2919 * Handles successful AJAX requests.
2921 * @param object data
2922 * @param string textStatus
2923 * @param jQuery jqXHR
2925 _success: function(data
, textStatus
, jqXHR
) {
2926 if (data
=== null) {
2930 if (data
.count
!== undefined) {
2931 var $fullQuoteObjectIDs
= (data
.fullQuoteObjectIDs
!== undefined) ? data
.fullQuoteObjectIDs
: { };
2932 this.updateCount(data
.count
, $fullQuoteObjectIDs
);
2935 if (data
.template
!== undefined) {
2936 if ($.trim(data
.template
) == '') {
2937 this.updateCount(0, { });
2940 this.renderDialog(data
.template
);
2946 * Returns true if pasting is supported.
2950 supportPaste: function() {
2951 return this._supportPaste
;
2956 * Namespace for message sharing related classes.
2958 WCF
.Message
.Share
= { };
2961 * Displays a dialog overlay for permalinks.
2963 WCF
.Message
.Share
.Content
= Class
.extend({
2965 * list of cached templates
2977 * Initializes the WCF.Message.Share.Content class.
2981 this._dialog
= null;
2985 WCF
.DOMNodeInsertedHandler
.addCallback('WCF.Message.Share.Content', $.proxy(this._initLinks
, this));
2989 * Initializes share links.
2991 _initLinks: function() {
2992 $('a.jsButtonShare').removeClass('jsButtonShare').click($.proxy(this._click
, this));
2996 * Displays links to share this content.
2998 * @param object event
3000 _click: function(event
) {
3001 event
.preventDefault();
3003 var $target
= $(event
.currentTarget
);
3004 var $link
= $target
.prop('href');
3005 var $title
= ($target
.data('linkTitle') ? $target
.data('linkTitle') : $link
);
3006 var $key
= $link
.hashCode();
3007 if (this._cache
[$key
] === undefined) {
3008 // remove dialog contents
3009 var $dialogInitialized
= false;
3010 if (this._dialog
=== null) {
3011 this._dialog
= $('<div />').hide().appendTo(document
.body
);
3012 $dialogInitialized
= true;
3015 this._dialog
.empty();
3018 // permalink (plain text)
3019 var $fieldset
= $('<fieldset><legend><label for="__sharePermalink">' + WCF
.Language
.get('wcf.message.share.permalink') + '</label></legend></fieldset>').appendTo(this._dialog
);
3020 $('<input type="text" id="__sharePermalink" class="long" readonly="readonly" />').attr('value', $link
).appendTo($fieldset
);
3022 // permalink (BBCode)
3023 var $fieldset
= $('<fieldset><legend><label for="__sharePermalinkBBCode">' + WCF
.Language
.get('wcf.message.share.permalink.bbcode') + '</label></legend></fieldset>').appendTo(this._dialog
);
3024 $('<input type="text" id="__sharePermalinkBBCode" class="long" readonly="readonly" />').attr('value', '[url=\'' + $link
+ '\']' + $title
+ '[/url]').appendTo($fieldset
);
3027 var $fieldset
= $('<fieldset><legend><label for="__sharePermalinkHTML">' + WCF
.Language
.get('wcf.message.share.permalink.html') + '</label></legend></fieldset>').appendTo(this._dialog
);
3028 $('<input type="text" id="__sharePermalinkHTML" class="long" readonly="readonly" />').attr('value', '<a href="' + $link
+ '">' + WCF
.String
.escapeHTML($title
) + '</a>').appendTo($fieldset
);
3030 this._cache
[$key
] = this._dialog
.html();
3032 if ($dialogInitialized
) {
3033 this._dialog
.wcfDialog({
3034 title
: WCF
.Language
.get('wcf.message.share')
3038 this._dialog
.wcfDialog('open');
3042 this._dialog
.html(this._cache
[$key
]).wcfDialog('open');
3045 this._enableSelection();
3049 * Enables text selection.
3051 _enableSelection: function() {
3052 var $inputElements
= this._dialog
.find('input').click(function() { $(this).select(); });
3054 // Safari on iOS can only select the text if it is not readonly and setSelectionRange() is used
3055 if (navigator
.userAgent
.match(/iP(ad|hone|od)/)) {
3056 $inputElements
.keydown(function() { return false; }).removeAttr('readonly').click(function() { this.setSelectionRange(0, 9999); });
3062 * Provides buttons to share a page through multiple social community sites.
3064 * @param boolean fetchObjectCount
3065 * @param object privacySettings
3067 WCF
.Message
.Share
.Page
= Class
.extend({
3075 * true if share count should be fetched
3078 _fetchObjectCount
: false,
3084 _pageDescription
: '',
3087 * canonical page URL
3093 * list of privacy settings per social media site
3095 _privacySettings
: { },
3098 * list of provider links and share callback
3099 * @var object<object>
3105 * @var WCF.Action.Proxy
3110 * Initializes the WCF.Message.Share.Page class.
3112 * @param boolean fetchObjectCount
3113 * @param object privacySettings
3115 init: function(fetchObjectCount
, privacySettings
) {
3116 this._dialog
= null;
3117 this._fetchObjectCount
= (fetchObjectCount
=== true) ? true : false;
3118 this._pageDescription
= encodeURIComponent($('meta[property="og:title"]').prop('content'));
3119 this._pageURL
= encodeURIComponent($('meta[property="og:url"]').prop('content'));
3120 this._privacySettings
= $.extend({
3125 }, privacySettings
|| { });
3128 this._initProvider();
3132 * Initializes all social media providers.
3134 _initProvider: function() {
3135 var $container
= $('.messageShareButtons');
3139 fetch: function() { self
._fetchFacebook(); },
3140 link
: $container
.find('.jsShareFacebook'),
3141 share: function() { self
._share('facebook', 'https://www.facebook.com/sharer.php?u={pageURL}&t={text}', true); }
3145 link
: $container
.find('.jsShareGoogle'),
3146 share: function() { self
._share('google', 'https://plus.google.com/share?url={pageURL}', true); }
3149 fetch: function() { self
._fetchReddit(); },
3150 link
:$container
.find('.jsShareReddit'),
3151 share: function() { self
._share('reddit', 'https://ssl.reddit.com/submit?url={pageURL}', true); }
3154 fetch: function() { self
._fetchTwitter(); },
3155 link
: $container
.find('.jsShareTwitter'),
3156 share: function() { self
._share('twitter', 'https://twitter.com/share?url={pageURL}&text={text}', false); }
3160 $.each(this._provider
, function(provider
, data
) {
3161 if (self
._privacySettings
[provider
]) {
3162 if (self
._fetchObjectCount
&& data
.fetch
) {
3167 data
.link
.addClass('disabled');
3170 data
.link
.data('provider', provider
).click($.proxy(self
._click
, self
));
3173 if (WCF
.User
.userID
) {
3174 var $openSettings
= $('<li class="jsShowPrivacySettings"><a><span class="icon icon32 fa-gear jsTooltip" title="' + WCF
.Language
.get('wcf.message.share.privacy') + '" /></a></li>');
3175 $openSettings
.appendTo($container
.children('ul')).children('a').click($.proxy(this._openPrivacySettings
, this));
3180 * Handles clicks on a social media provider link.
3182 * @param object event
3184 _click: function(event
) {
3185 var $link
= $(event
.currentTarget
);
3186 var $provider
= $link
.data('provider');
3188 if ($link
.hasClass('disabled')) {
3189 if (WCF
.User
.userID
) {
3190 this._openPrivacySettings();
3193 // guest => enable button
3194 $link
.removeClass('disabled');
3198 this._provider
[$provider
].share();
3203 * Opens the privacy settings dialog.
3205 _openPrivacySettings: function() {
3206 if (this._proxy
=== null) {
3207 this._proxy
= new WCF
.Action
.Proxy({
3208 success
: $.proxy(this._success
, this)
3212 this._proxy
.setOption('data', {
3213 actionName
: 'getSocialNetworkPrivacySettings',
3214 className
: 'wcf\\data\\user\\UserAction'
3216 this._proxy
.sendRequest();
3220 * Handles successful AJAX requests.
3222 * @param object data
3223 * @param string textStatus
3224 * @param jQuery jqXHR
3226 _success: function(data
, textStatus
, jqXHR
) {
3227 switch (data
.actionName
) {
3228 case 'getSocialNetworkPrivacySettings':
3229 this._renderDialog(data
);
3232 case 'saveSocialNetworkPrivacySettings':
3233 this._updatePrivacySettings(data
);
3239 * Renders the settings dialog.
3241 * @param object data
3243 _renderDialog: function(data
) {
3244 if (this._dialog
=== null) {
3245 this._dialog
= $('<div />').hide().appendTo(document
.body
);
3246 this._dialog
.html(data
.returnValues
.template
);
3247 this._dialog
.wcfDialog({
3248 title
: WCF
.Language
.get('wcf.message.share.privacy')
3252 this._dialog
.html(data
.returnValues
.template
);
3253 this._dialog
.wcfDialog('open');
3256 this._dialog
.find('input[type=submit]').click($.proxy(this._save
, this));
3263 this._proxy
.setOption('data', {
3264 actionName
: 'saveSocialNetworkPrivacySettings',
3265 className
: 'wcf\\data\\user\\UserAction',
3267 facebook
: (this._dialog
.find('input[name=facebook]').is(':checked')),
3268 google
: (this._dialog
.find('input[name=google]').is(':checked')),
3269 reddit
: (this._dialog
.find('input[name=reddit]').is(':checked')),
3270 twitter
: (this._dialog
.find('input[name=twitter]').is(':checked'))
3273 this._proxy
.sendRequest();
3275 this._dialog
.wcfDialog('close');
3279 * Updates the internal privacy settings.
3281 * @param object data
3283 _updatePrivacySettings: function(data
) {
3284 this._privacySettings
= $.extend(this._privacySettings
, data
.returnValues
.settings
);
3287 $.each(data
.returnValues
.settings
, function(provider
, status
) {
3288 self
._privacySettings
[provider
] = (status
) ? true : false;
3291 self
._provider
[provider
].link
.removeClass('disabled');
3293 if (self
._fetchObjectCount
&& self
._provider
[provider
].fetch
) {
3294 self
._provider
[provider
].fetch();
3298 self
._provider
[provider
].link
.addClass('disabled');
3302 new WCF
.System
.Notification().show();
3306 * Shares current page to selected social community site.
3308 * @param string objectName
3310 * @param boolean appendURL
3312 _share: function(objectName
, url
, appendURL
) {
3313 window
.open(url
.replace(/{pageURL}/, this._pageURL
).replace(/{text}/, this._pageDescription
+ (appendURL
? " " + this._pageURL
: "")), objectName
, 'height=600,width=600');
3317 * Fetches share count from a social community site.
3320 * @param object callback
3321 * @param string callbackName
3323 _fetchCount: function(url
, callback
, callbackName
) {
3327 showLoadingOverlay
: false,
3329 suppressErrors
: true,
3331 url
: url
.replace(/{pageURL}/, this._pageURL
)
3334 $options
.jsonp
= callbackName
;
3337 new WCF
.Action
.Proxy($options
);
3341 * Fetches number of Facebook likes.
3343 _fetchFacebook: function() {
3344 this._fetchCount('https://graph.facebook.com/?id={pageURL}', $.proxy(function(data
) {
3346 this._provider
.facebook
.link
.children('span.badge').show().text(data
.shares
);
3352 * Fetches tweet count from Twitter.
3354 _fetchTwitter: function() {
3355 if (window
.location
.protocol
.match(/^https/)) return;
3357 this._fetchCount('http://urls.api.twitter.com/1/urls/count.json?url={pageURL}', $.proxy(function(data
) {
3359 this._provider
.twitter
.link
.children('span.badge').show().text(data
.count
);
3365 * Fetches cumulative vote sum from Reddit.
3367 _fetchReddit: function() {
3368 if (window
.location
.protocol
.match(/^https/)) return;
3370 this._fetchCount('http://www.reddit.com/api/info.json?url={pageURL}', $.proxy(function(data
) {
3371 if (data
.data
.children
.length
) {
3372 this._provider
.reddit
.link
.children('span.badge').show().text(data
.data
.children
[0].data
.score
);
3379 * Handles user mention suggestions in Redactor instances.
3381 * Important: Objects of this class have to be created before Redactor
3384 WCF
.Message
.UserMention
= Class
.extend({
3386 * current caret position
3389 _caretPosition
: null,
3392 * name of the class used to get the user suggestions
3395 _className
: 'wcf\\data\\user\\UserAction',
3404 * dropdown menu object
3407 _dropdownMenu
: null,
3410 * suggestion item index, -1 if none is selected
3422 * current beginning of the mentioning
3428 * redactor instance object
3434 * delay timer to only send requests after user paused typing
3435 * @var WCF.PeriodicalExecuter
3440 * Initalizes user suggestions for Redactor with the given textarea id.
3442 * @param string wysiwygSelector
3444 init: function(wysiwygSelector
) {
3445 if ($.browser
.mobile
&& $.browser
.mozilla
) {
3446 // the desktop Firefox work-arounds do not work on Firefox for Android, in fact they crash it
3450 this._textarea
= $('#' + wysiwygSelector
);
3451 this._redactor
= this._textarea
.redactor('core.getObject');
3453 this._dropdown
= this._textarea
.redactor('core.getEditor');
3454 this._dropdownMenu
= $('<ul class="dropdownMenu userSuggestionList" />').appendTo(this._textarea
.parent());
3455 WCF
.Dropdown
.initDropdownFragment(this._dropdown
, this._dropdownMenu
);
3457 this._proxy
= new WCF
.Action
.Proxy({
3458 autoAbortPrevious
: true,
3459 success
: $.proxy(this._success
, this)
3462 WCF
.System
.Event
.addListener('com.woltlab.wcf.redactor', 'keydown_' + wysiwygSelector
, $.proxy(this._keydown
, this));
3463 WCF
.System
.Event
.addListener('com.woltlab.wcf.redactor', 'keyup_' + wysiwygSelector
, $.proxy(this._keyup
, this));
3467 * Clears the suggestion list.
3469 _clearList: function() {
3472 this._dropdownMenu
.empty();
3476 * Handles a click on a list item suggesting a username.
3478 * This function is also called when seleting a suggested username by clicking
3481 * @param object event
3483 _click: function(event
) {
3484 // in Firefox, this._caretPosition does not have the text node as
3485 // startContainer anymore when confirming a username suggestion by
3486 // clicking enter, thus we need to manually adjust it
3487 if ($.browser
.mozilla
&& this._caretPosition
.startContainer
.nodeName
== 'P') {
3488 var $textNode
= this._caretPosition
.startContainer
.childNodes
[this._caretPosition
.startOffset
- 1];
3490 this._caretPosition
= document
.createRange();
3491 this._caretPosition
.selectNodeContents($textNode
);
3492 this._caretPosition
.collapse();
3495 // restore caret position
3496 this._redactor
.wutil
.replaceRangesWith(this._caretPosition
);
3498 this._setUsername($(event
.currentTarget
).data('username'));
3502 * Creates an item in the suggestion list with the given data.
3506 _createListItem: function(listItemData
) {
3507 var $listItem
= $('<li />').data('username', listItemData
.label
).click($.proxy(this._click
, this)).appendTo(this._dropdownMenu
);
3509 var $box16
= $('<div />').addClass('box16').appendTo($listItem
);
3510 $box16
.append($(listItemData
.icon
).addClass('framed'));
3511 $box16
.append($('<div />').append($('<span />').text(listItemData
.label
)));
3515 * Returns the offsets used to set the position of the user suggestion
3520 _getDropdownMenuPosition: function() {
3521 var $orgRange
= getSelection().getRangeAt(0).cloneRange();
3523 // mark the entire text, starting from the '@' to the current cursor position
3524 var $newRange
= document
.createRange();
3525 $newRange
.setStart($orgRange
.startContainer
, $orgRange
.startOffset
- (this._mentionStart
.length
+ 1));
3526 $newRange
.setEnd($orgRange
.startContainer
, $orgRange
.startOffset
);
3528 this._redactor
.wutil
.replaceRangesWith($newRange
);
3530 // get the offsets of the bounding box of current text selection
3531 var $range
= getSelection().getRangeAt(0);
3532 var $rect
= $range
.getBoundingClientRect();
3533 var $window
= $(window
);
3535 top
: Math
.round($rect
.bottom
) + $window
.scrollTop(),
3536 left
: Math
.round($rect
.left
) + $window
.scrollLeft()
3539 if (this._lineHeight
=== null) {
3540 this._lineHeight
= Math
.round($rect
.bottom
- $rect
.top
);
3543 // restore caret position
3544 this._redactor
.wutil
.replaceRangesWith($orgRange
);
3545 this._caretPosition
= $orgRange
;
3551 * Replaces the started mentioning with a chosen username.
3553 _setUsername: function(username
) {
3554 if (this._timer
!== null) {
3558 this._proxy
.abortPrevious();
3560 var $orgRange
= getSelection().getRangeAt(0).cloneRange();
3562 // allow redactor to undo this
3563 this._redactor
.buffer
.set();
3565 var $newRange
= document
.createRange();
3566 $newRange
.setStart($orgRange
.startContainer
, $orgRange
.startOffset
- (this._mentionStart
.length
+ 1));
3567 $newRange
.setEnd($orgRange
.startContainer
, $orgRange
.startOffset
);
3569 this._redactor
.wutil
.replaceRangesWith($newRange
);
3571 var $range
= getSelection().getRangeAt(0);
3572 $range
.deleteContents();
3573 $range
.collapse(true);
3576 if (username
.indexOf("'") !== -1) {
3577 username
= username
.replace(/'/g, "''");
3579 username = "'" + username + "'";
3581 // use native API to prevent issues in Internet Explorer
3582 var $text = document.createTextNode('@' + username);
3583 $range.insertNode($text);
3585 var $newRange = document.createRange();
3586 $newRange.setStart($text, username.length + 1);
3587 $newRange.setEnd($text, username.length + 1);
3589 this._redactor.wutil.replaceRangesWith($newRange);
3595 * Returns the parameters for the AJAX request.
3599 _getParameters: function() {
3602 includeUserGroups: false,
3603 searchString: this._mentionStart
3609 * Returns the relevant text in front of the caret in the current line.
3613 _getTextLineInFrontOfCaret: function() {
3614 // if text is marked, user suggestions are disabled
3615 if (this._redactor.selection.getHtml().length) {
3619 var $range = getSelection().getRangeAt(0);
3621 // in Firefox, blurring and refocusing the browser creates separate
3623 if ($.browser.mozilla && $range.startContainer.nodeType == 3) {
3624 $range.startContainer.parentNode.normalize();
3627 var $text = $range.startContainer.textContent.substr(0, $range.startOffset);
3629 // remove unicode zero width space and non-breaking space
3630 var $textBackup = $text;
3632 var $hadSpace = false;
3633 for (var $i = 0; $i < $textBackup.length; $i++) {
3634 var $byte = $textBackup.charCodeAt($i).toString(16);
3635 if ($byte != '200b
' && (!/\s/.test($textBackup[$i]) || (($byte == 'a0
' || $byte == '20') && !$hadSpace))) {
3636 if ($byte == 'a0
' || $byte == '20') {
3640 if ($textBackup[$i] === '@' && $i && /\s/.test($textBackup[$i - 1])) {
3645 $text += $textBackup[$i];
3657 * Hides the suggestion list.
3659 _hideList: function() {
3660 this._dropdown.removeClass('dropdownOpen
');
3661 this._dropdownMenu.removeClass('dropdownOpen
');
3663 this._itemIndex = -1;
3667 * Handles the keydown event to check if the user starts mentioning someone.
3669 * @param object data
3671 _keydown: function(data) {
3672 if (this._redactor.wutil.inPlainMode()) {
3676 if (this._dropdownMenu.is(':visible
')) {
3677 switch (data.event.which) {
3678 case $.ui.keyCode.ENTER:
3679 data.event.preventDefault();
3682 this._dropdownMenu.children('li
').eq(this._itemIndex).trigger('click
');
3685 case $.ui.keyCode.UP:
3687 data.event.preventDefault();
3689 this._selectItem(this._itemIndex - 1);
3692 case $.ui.keyCode.DOWN:
3694 data.event.preventDefault();
3696 this._selectItem(this._itemIndex + 1);
3703 * Handles the keyup event to check if the user starts mentioning someone.
3705 * @param object data
3707 _keyup: function(data) {
3708 if (this._redactor.wutil.inPlainMode()) {
3712 // abort previous search requests
3713 if (this._timer !== null) {
3717 this._proxy.abortPrevious();
3719 // ignore enter key up event
3720 if (data.event.which === $.ui.keyCode.ENTER) {
3724 // ignore event if suggestion list and user pressed enter, arrow up or arrow down
3725 if (this._dropdownMenu.is(':visible
') && data.event.which in { 13:1, 38:1, 40:1 }) {
3729 var $currentText = this._getTextLineInFrontOfCaret();
3731 var $match = $currentText.match(/@([^,]{3,})$/);
3733 // if mentioning is at text begin or there's a whitespace character
3734 // before the '@', everything is fine
3735 if (!$match
.index
|| $currentText
[$match
.index
- 1].match(/\s/)) {
3736 this._mentionStart
= $match
[1];
3738 if (this._timer
!== null) {
3742 this._timer
= new WCF
.PeriodicalExecuter($.proxy(function() {
3743 this._proxy
.setOption('data', {
3744 actionName
: 'getSearchResultList',
3745 className
: this._className
,
3746 interfaceName
: 'wcf\\data\\ISearchAction',
3747 parameters
: this._getParameters()
3749 this._proxy
.sendRequest();
3766 * Selects the suggestion with the given item index.
3768 * @param integer itemIndex
3770 _selectItem: function(itemIndex
) {
3771 var $li
= this._dropdownMenu
.children('li');
3773 if (itemIndex
< 0) {
3774 itemIndex
= $li
.length
- 1;
3776 else if (itemIndex
+ 1 > $li
.length
) {
3780 $li
.removeClass('dropdownNavigationItem');
3781 $li
.eq(itemIndex
).addClass('dropdownNavigationItem');
3783 this._itemIndex
= itemIndex
;
3787 * Shows the suggestion list.
3789 _showList: function() {
3790 this._dropdown
.addClass('dropdownOpen');
3791 this._dropdownMenu
.addClass('dropdownOpen');
3795 * Evalutes user suggestion-AJAX request results.
3797 * @param object data
3798 * @param string textStatus
3799 * @param jQuery jqXHR
3801 _success: function(data
, textStatus
, jqXHR
) {
3802 this._clearList(false);
3804 if ($.getLength(data
.returnValues
)) {
3805 for (var $i
in data
.returnValues
) {
3806 var $item
= data
.returnValues
[$i
];
3807 this._createListItem($item
);
3810 this._updateSuggestionListPosition();
3816 * Updates the position of the suggestion list.
3818 _updateSuggestionListPosition: function() {
3820 var $dropdownMenuPosition
= this._getDropdownMenuPosition();
3821 $dropdownMenuPosition
.top
+= 5; // add a little vertical gap
3823 this._dropdownMenu
.css($dropdownMenuPosition
);
3824 this._selectItem(0);
3826 if ($dropdownMenuPosition
.top
+ this._dropdownMenu
.outerHeight() + 10 > $(window
).height() + $(document
).scrollTop()) {
3827 this._dropdownMenu
.addClass('dropdownArrowBottom');
3829 this._dropdownMenu
.css({
3830 top
: $dropdownMenuPosition
.top
- this._dropdownMenu
.outerHeight() - 2 * this._lineHeight
+ 5
3834 this._dropdownMenu
.removeClass('dropdownArrowBottom');
3838 // ignore errors that are caused by pressing enter to
3839 // often in a short period of time
3845 * Provides a specialized tab menu used for message options, integrates better into the editor.
3847 $.widget('wcf.messageTabMenu', {
3849 * list of existing tabs and their containers
3850 * @var array<object>
3855 * list of tab names and their corresponding index
3856 * @var object<string>
3862 * @var object<mixed>
3869 * Creates the message tab menu.
3871 _create: function() {
3872 var $tabs
= this.element
.find('> nav > ul > li:not(.jsFlexibleMenuDropdown)');
3873 var $tabContainers
= this.element
.find('> div, > fieldset');
3875 if ($tabs
.length
!= $tabContainers
.length
) {
3876 console
.debug("[wcf.messageTabMenu] Amount of tabs does not equal amount of tab containers, aborting.");
3880 var $preselect
= this.element
.data('preselect');
3882 this._tabsByName
= { };
3883 for (var $i
= 0; $i
< $tabs
.length
; $i
++) {
3884 var $tab
= $($tabs
[$i
]);
3885 var $tabContainer
= $($tabContainers
[$i
]);
3887 var $name
= $tab
.data('name');
3888 if ($name
=== undefined) {
3889 var $href
= $tab
.children('a').prop('href');
3890 if ($href
!== undefined) {
3891 if ($href
.match(/#([a-zA-Z_-]+)$/)) {
3896 if ($name
=== undefined) {
3897 $name
= $tab
.wcfIdentify();
3898 console
.debug("[wcf.messageTabMenu] Missing name attribute, assuming generic ID '" + $name
+ "'");
3903 container
: $tabContainer
,
3907 this._tabsByName
[$name
] = $i
;
3909 var $anchor
= $tab
.children('a').data('index', $i
).click($.proxy(this._showTab
, this));
3910 if ($preselect
== $name
) {
3911 $anchor
.trigger('click');
3915 if ($preselect
=== true && this._tabs
.length
) {
3916 // pick the first available tab
3917 this._tabs
[0].tab
.children('a').trigger('click');
3920 var $collapsible
= this.element
.data('collapsible');
3921 if ($collapsible
!== undefined) {
3922 this.options
.collapsible
= $collapsible
;
3927 * Destroys the message tab menu.
3929 destroy: function() {
3930 $.Widget
.prototype.destroy
.apply(this, arguments
);
3932 this.element
.remove();
3936 * Shows a tab or collapses it if already open.
3938 * @param object event
3939 * @param integer index
3940 * @param boolean forceOpen
3942 _showTab: function(event
, index
, forceOpen
) {
3943 var $index
= (event
=== null) ? index
: $(event
.currentTarget
).data('index');
3944 forceOpen
= (!this.options
.collapsible
|| forceOpen
=== true) ? true : false;
3947 for (var $i
= 0; $i
< this._tabs
.length
; $i
++) {
3948 var $current
= this._tabs
[$i
];
3951 if (!$current
.tab
.hasClass('active')) {
3952 $current
.tab
.addClass('active');
3953 $current
.container
.addClass('active');
3958 else if (forceOpen
=== true) {
3963 $current
.tab
.removeClass('active');
3964 $current
.container
.removeClass('active');
3967 if (event
!== null) {
3968 event
.preventDefault();
3969 event
.stopPropagation();
3972 if ($target
!== null) {
3973 this._trigger('show', { }, {
3980 * Toggle a specific tab by either index or name property.
3982 * @param mixed index
3983 * @param boolean forceOpen
3985 showTab: function(index
, forceOpen
) {
3986 if (!$.isNumeric(index
)) {
3987 if (this._tabsByName
[index
] !== undefined) {
3988 index
= this._tabsByName
[index
];
3992 if (this._tabs
[index
] === undefined) {
3993 console
.debug("[wcf.messageTabMenu] Cannot locate tab identified by '" + index
+ "'");
3997 this._showTab(null, index
, forceOpen
);
4001 * Returns a tab by it's unique name.
4003 * @param string name
4006 getTab: function(name
) {
4007 if (this._tabsByName
[name
] !== undefined) {
4008 return this._tabs
[this._tabsByName
[name
]].tab
;
4015 * Returns a tab container by it's tab's unique name.
4017 * @param string name
4020 getContainer: function(name
) {
4021 if (this._tabsByName
[name
] !== undefined) {
4022 return this._tabs
[this._tabsByName
[name
]].container
;