Merge remote-tracking branch 'refs/remotes/origin/3.0'
authorAlexander Ebert <ebert@woltlab.com>
Mon, 12 Feb 2018 17:38:00 +0000 (18:38 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 12 Feb 2018 17:38:00 +0000 (18:38 +0100)
# Conflicts:
# com.woltlab.wcf/package.xml
# wcfsetup/install/files/js/WCF.Combined.min.js
# wcfsetup/install/files/js/WCF.Message.js
# wcfsetup/install/files/lib/system/WCF.class.php
# wcfsetup/install/lang/de.xml
# wcfsetup/install/lang/en.xml

1  2 
wcfsetup/install/files/js/WCF.Message.js
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index dbd117260c3a68e3a7fdde5b27cea5f76d36a55f,060c4318e3dc88434c2be9d7e6a838b67c7d4c0b..b786173098f4520bccb34744fe4988b26175b1eb
@@@ -312,1709 -267,1539 +312,1728 @@@ WCF.Message.FormGuard = Class.extend(
        }
  });
  
 -/**
 - * Provides previews for Redactor message fields.
 - * 
 - * @param     string          className
 - * @param     string          messageFieldID
 - * @param     string          previewButtonID
 - */
 -WCF.Message.Preview = Class.extend({
 -      /**
 -       * class name
 -       * @var string
 -       */
 -      _className: '',
 -      
 -      /**
 -       * message field id
 -       * @var string
 -       */
 -      _messageFieldID: '',
 -      
 -      /**
 -       * message field
 -       * @var jQuery
 -       */
 -      _messageField: null,
 -      
 -      /**
 -       * action proxy
 -       * @var WCF.Action.Proxy
 -       */
 -      _proxy: null,
 -      
 -      /**
 -       * preview button
 -       * @var jQuery
 -       */
 -      _previewButton: null,
 -      
 -      /**
 -       * previous button label
 -       * @var string
 -       */
 -      _previewButtonLabel: '',
 -      
 -      /**
 -       * Initializes a new WCF.Message.Preview object.
 -       * 
 -       * @param       string          className
 -       * @param       string          messageFieldID
 -       * @param       string          previewButtonID
 -       */
 -      init: function(className, messageFieldID, previewButtonID) {
 -              this._className = className;
 -              
 -              // validate message field
 -              this._messageFieldID = $.wcfEscapeID(messageFieldID);
 -              this._textarea = $('#' + this._messageFieldID);
 -              if (!this._textarea.length) {
 -                      console.debug("[WCF.Message.Preview] Unable to find message field identified by '" + this._messageFieldID + "'");
 -                      return;
 -              }
 -              
 -              // validate preview button
 -              previewButtonID = $.wcfEscapeID(previewButtonID);
 -              this._previewButton = $('#' + previewButtonID);
 -              if (!this._previewButton.length) {
 -                      console.debug("[WCF.Message.Preview] Unable to find preview button identified by '" + previewButtonID + "'");
 -                      return;
 +if (COMPILER_TARGET_DEFAULT) {
 +      /**
 +       * Provides previews for Redactor message fields.
 +       *
 +       * @param        string                className
 +       * @param        string                messageFieldID
 +       * @param        string                previewButtonID
 +       */
 +      WCF.Message.Preview = Class.extend({
 +              /**
 +               * class name
 +               * @var        string
 +               */
 +              _className: '',
 +              
 +              /**
 +               * message field id
 +               * @var        string
 +               */
 +              _messageFieldID: '',
 +              
 +              /**
 +               * message field
 +               * @var        jQuery
 +               */
 +              _messageField: null,
 +              
 +              /**
 +               * action proxy
 +               * @var        WCF.Action.Proxy
 +               */
 +              _proxy: null,
 +              
 +              /**
 +               * preview button
 +               * @var        jQuery
 +               */
 +              _previewButton: null,
 +              
 +              /**
 +               * previous button label
 +               * @var        string
 +               */
 +              _previewButtonLabel: '',
 +              
 +              /**
 +               * Initializes a new WCF.Message.Preview object.
 +               *
 +               * @param        string                className
 +               * @param        string                messageFieldID
 +               * @param        string                previewButtonID
 +               */
 +              init: function (className, messageFieldID, previewButtonID) {
 +                      this._className = className;
 +                      
 +                      // validate message field
 +                      this._messageFieldID = $.wcfEscapeID(messageFieldID);
 +                      this._textarea = $('#' + this._messageFieldID);
 +                      if (!this._textarea.length) {
 +                              console.debug("[WCF.Message.Preview] Unable to find message field identified by '" + this._messageFieldID + "'");
 +                              return;
 +                      }
 +                      
 +                      // validate preview button
 +                      previewButtonID = $.wcfEscapeID(previewButtonID);
 +                      this._previewButton = $('#' + previewButtonID);
 +                      if (!this._previewButton.length) {
 +                              console.debug("[WCF.Message.Preview] Unable to find preview button identified by '" + previewButtonID + "'");
 +                              return;
 +                      }
 +                      
 +                      this._previewButton.click($.proxy(this._click, this));
 +                      this._proxy = new WCF.Action.Proxy({
 +                              failure: $.proxy(this._failure, this),
 +                              success: $.proxy(this._success, this)
 +                      });
 +              },
 +              
 +              /**
 +               * Reads message field input and triggers an AJAX request.
 +               */
 +              _click: function (event) {
 +                      var $message = this._getMessage();
 +                      if ($message === null) {
 +                              console.debug("[WCF.Message.Preview] Unable to access Redactor instance of '" + this._messageFieldID + "'");
 +                              return;
 +                      }
 +                      
 +                      this._proxy.setOption('data', {
 +                              actionName: 'getMessagePreview',
 +                              className: this._className,
 +                              parameters: this._getParameters($message)
 +                      });
 +                      this._proxy.sendRequest();
 +                      
 +                      // update button label
 +                      this._previewButtonLabel = this._previewButton.html();
 +                      this._previewButton.html(WCF.Language.get('wcf.global.loading')).disable();
 +                      
 +                      // poke event
 +                      event.stopPropagation();
 +                      return false;
 +              },
 +              
 +              /**
 +               * Returns request parameters.
 +               *
 +               * @param        string                message
 +               * @return        object
 +               */
 +              _getParameters: function (message) {
 +                      // collect message form options
 +                      var $options = {};
 +                      $('#settings_' + this._messageFieldID).find('input[type=checkbox]').each(function (index, checkbox) {
 +                              var $checkbox = $(checkbox);
 +                              if ($checkbox.is(':checked')) {
 +                                      $options[$checkbox.prop('name')] = $checkbox.prop('value');
 +                              }
 +                      });
 +                      
 +                      // build parameters
 +                      return {
 +                              data: {
 +                                      message: message
 +                              },
 +                              options: $options
 +                      };
 +              },
 +              
 +              /**
 +               * Returns parsed message from Redactor or null if editor was not accessible.
 +               *
 +               * @return        string
 +               */
 +              _getMessage: function () {
 +                      return this._textarea.redactor('code.get');
 +              },
 +              
 +              /**
 +               * Handles successful AJAX requests.
 +               *
 +               * @param        object                data
 +               * @param        string                textStatus
 +               * @param        jQuery                jqXHR
 +               */
 +              _success: function (data, textStatus, jqXHR) {
 +                      // restore preview button
 +                      this._previewButton.html(this._previewButtonLabel).enable();
 +                      
 +                      // remove error message
 +                      this._textarea.parent().children('small.innerError').remove();
 +                      
 +                      // evaluate message
 +                      this._handleResponse(data);
 +              },
 +              
 +              /**
 +               * Evaluates response data.
 +               *
 +               * @param        object                data
 +               */
 +              _handleResponse: function (data) {
 +              },
 +              
 +              /**
 +               * Handles errors during preview requests.
 +               *
 +               * The return values indicates if the default error overlay is shown.
 +               *
 +               * @param        object                data
 +               * @return        boolean
 +               */
 +              _failure: function (data) {
 +                      if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
 +                              return true;
 +                      }
 +                      
 +                      // restore preview button
 +                      this._previewButton.html(this._previewButtonLabel).enable();
 +                      
 +                      var $innerError = this._textarea.parent().children('small.innerError').empty();
 +                      if (!$innerError.length) {
 +                              $innerError = $('<small class="innerError" />').appendTo(this._textarea.parent());
 +                      }
 +                      
 +                      var message = (data.returnValues.errorType === 'empty' ? WCF.Language.get('wcf.global.form.error.empty') : data.returnValues.errorMessage);
 +                      if (data.returnValues.realErrorMessage) message = data.returnValues.realErrorMessage;
 +                      $innerError.html(message);
 +                      
 +                      return false;
                }
 -              
 -              this._previewButton.click($.proxy(this._click, this));
 -              this._proxy = new WCF.Action.Proxy({
 -                      failure: $.proxy(this._failure, this),
 -                      success: $.proxy(this._success, this)
 -              });
 -      },
 +      });
        
        /**
 -       * Reads message field input and triggers an AJAX request.
 +       * Default implementation for message previews.
 +       *
 +       * @see        WCF.Message.Preview
         */
 -      _click: function(event) {
 -              var $message = this._getMessage();
 -              if ($message === null) {
 -                      console.debug("[WCF.Message.Preview] Unable to access Redactor instance of '" + this._messageFieldID + "'");
 -                      return;
 -              }
 +      WCF.Message.DefaultPreview = WCF.Message.Preview.extend({
 +              _dialog: null,
 +              _options: {},
                
 -              this._proxy.setOption('data', {
 -                      actionName: 'getMessagePreview',
 -                      className: this._className,
 -                      parameters: this._getParameters($message)
 -              });
 -              this._proxy.sendRequest();
 -              
 -              // update button label
 -              this._previewButtonLabel = this._previewButton.html();
 -              this._previewButton.html(WCF.Language.get('wcf.global.loading')).disable();
 -              
 -              // poke event
 -              event.stopPropagation();
 -              return false;
 -      },
 -      
 -      /**
 -       * Returns request parameters.
 -       * 
 -       * @param       string          message
 -       * @return      object
 -       */
 -      _getParameters: function(message) {
 -              // collect message form options
 -              var $options = { };
 -              $('#settings_' + this._messageFieldID).find('input[type=checkbox]').each(function(index, checkbox) {
 -                      var $checkbox = $(checkbox);
 -                      if ($checkbox.is(':checked')) {
 -                              $options[$checkbox.prop('name')] = $checkbox.prop('value');
 +              /**
 +               * @see        WCF.Message.Preview.init()
 +               */
 +              init: function (options) {
 +                      if (arguments.length > 1 && typeof options === 'string') {
 +                              throw new Error("Outdated API call, please update your implementation.");
 +                      }
 +                      
 +                      this._options = $.extend({
 +                              disallowedBBCodesPermission: 'user.message.disallowedBBCodes',
 +                              messageFieldID: '',
 +                              previewButtonID: '',
 +                              messageObjectType: '',
 +                              messageObjectID: 0
 +                      }, options);
 +                      
 +                      if (!this._options.messageObjectType) {
 +                              throw new Error("Field 'messageObjectType' cannot be empty.");
 +                      }
 +                      
 +                      this._super('wcf\\data\\bbcode\\MessagePreviewAction', this._options.messageFieldID, this._options.previewButtonID);
 +              },
 +              
 +              /**
 +               * @see        WCF.Message.Preview._handleResponse()
 +               */
 +              _handleResponse: function (data) {
 +                      require(['WoltLabSuite/Core/Ui/Dialog'], (function (UiDialog) {
 +                              UiDialog.open(this, '<div class="htmlContent">' + data.returnValues.message + '</div>');
 +                      }).bind(this));
 +              },
 +              
 +              /**
 +               * @see        WCF.Message.Preview._getParameters()
 +               */
 +              _getParameters: function (message) {
 +                      var $parameters = this._super(message);
 +                      
 +                      for (var key in this._options) {
 +                              if (this._options.hasOwnProperty(key) && key !== 'messageFieldID' && key !== 'previewButtonID') {
 +                                      $parameters[key] = this._options[key];
 +                              }
 +                      }
 +                      
 +                      return $parameters;
 +              },
 +              
 +              _dialogSetup: function () {
 +                      return {
 +                              id: 'messagePreview',
 +                              options: {
 +                                      title: WCF.Language.get('wcf.global.preview')
 +                              },
 +                              source: null
                        }
 -              });
 -              
 -              // build parameters
 -              return {
 -                      data: {
 -                              message: message
 -                      },
 -                      options: $options
 -              };
 -      },
 -      
 -      /**
 -       * Returns parsed message from Redactor or null if editor was not accessible.
 -       * 
 -       * @return      string
 -       */
 -      _getMessage: function() {
 -              return this._textarea.redactor('code.get');
 -      },
 -      
 -      /**
 -       * Handles successful AJAX requests.
 -       * 
 -       * @param       object          data
 -       * @param       string          textStatus
 -       * @param       jQuery          jqXHR
 -       */
 -      _success: function(data, textStatus, jqXHR) {
 -              // restore preview button
 -              this._previewButton.html(this._previewButtonLabel).enable();
 -              
 -              // remove error message
 -              this._textarea.parent().children('small.innerError').remove();
 -              
 -              // evaluate message
 -              this._handleResponse(data);
 -      },
 -      
 -      /**
 -       * Evaluates response data.
 -       * 
 -       * @param       object          data
 -       */
 -      _handleResponse: function(data) { },
 -      
 -      /**
 -       * Handles errors during preview requests.
 -       * 
 -       * The return values indicates if the default error overlay is shown.
 -       * 
 -       * @param       object          data
 -       * @return      boolean
 -       */
 -      _failure: function(data) {
 -              if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
 -                      return true;
 -              }
 -              
 -              // restore preview button
 -              this._previewButton.html(this._previewButtonLabel).enable();
 -              
 -              var $innerError = this._textarea.parent().children('small.innerError').empty();
 -              if (!$innerError.length) {
 -                      $innerError = $('<small class="innerError" />').appendTo(this._textarea.parent());
 -              }
 -              
 -              $innerError.html((data.returnValues.errorType === 'empty' ? WCF.Language.get('wcf.global.form.error.empty') : data.returnValues.errorMessage));
 -              
 -              return false;
 -      }
 -});
 -
 -/**
 - * Default implementation for message previews.
 - * 
 - * @see       WCF.Message.Preview
 - */
 -WCF.Message.DefaultPreview = WCF.Message.Preview.extend({
 -      _dialog: null,
 -      _options: {},
 -      
 -      /**
 -       * @see WCF.Message.Preview.init()
 -       */
 -      init: function(options) {
 -              if (arguments.length > 1 && typeof options === 'string') {
 -                      throw new Error("Outdated API call, please update your implementation.");
                }
 -              
 -              this._options = $.extend({
 -                      disallowedBBCodesPermission: 'user.message.disallowedBBCodes',
 -                      messageFieldID: '',
 -                      previewButtonID: '',
 -                      messageObjectType: '',
 -                      messageObjectID: 0
 -              }, options);
 -              
 -              if (!this._options.messageObjectType) {
 -                      throw new Error("Field 'messageObjectType' cannot be empty.");
 +      });
 +      
 +      /**
 +       * Handles multilingualism for messages.
 +       *
 +       * @param        integer                languageID
 +       * @param        object                availableLanguages
 +       * @param        boolean                forceSelection
 +       */
 +      WCF.Message.Multilingualism = Class.extend({
 +              /**
 +               * list of available languages
 +               * @var        object
 +               */
 +              _availableLanguages: {},
 +              
 +              /**
 +               * language id
 +               * @var        integer
 +               */
 +              _languageID: 0,
 +              
 +              /**
 +               * language input element
 +               * @var        jQuery
 +               */
 +              _languageInput: null,
 +              
 +              /**
 +               * Initializes WCF.Message.Multilingualism
 +               *
 +               * @param        integer                languageID
 +               * @param        object                availableLanguages
 +               * @param        boolean                forceSelection
 +               */
 +              init: function (languageID, availableLanguages, forceSelection) {
 +                      this._availableLanguages = availableLanguages;
 +                      this._languageID = languageID || 0;
 +                      
 +                      this._languageInput = $('#languageID');
 +                      
 +                      // preselect current language id
 +                      this._updateLabel();
 +                      
 +                      // register event listener
 +                      this._languageInput.find('.dropdownMenu > li').click($.proxy(this._click, this));
 +                      
 +                      // add element to disable multilingualism
 +                      if (!forceSelection) {
 +                              var $dropdownMenu = this._languageInput.find('.dropdownMenu');
 +                              $('<li class="dropdownDivider" />').appendTo($dropdownMenu);
 +                              $('<li><span><span class="badge">' + this._availableLanguages[0] + '</span></span></li>').click($.proxy(this._disable, this)).appendTo($dropdownMenu);
 +                      }
 +                      
 +                      // bind submit event
 +                      this._languageInput.parents('form').submit($.proxy(this._submit, this));
 +              },
 +              
 +              /**
 +               * Handles language selections.
 +               *
 +               * @param        object                event
 +               */
 +              _click: function (event) {
 +                      this._languageID = $(event.currentTarget).data('languageID');
 +                      this._updateLabel();
 +              },
 +              
 +              /**
 +               * Disables language selection.
 +               */
 +              _disable: function () {
 +                      this._languageID = 0;
 +                      this._updateLabel();
 +              },
 +              
 +              /**
 +               * Updates selected language.
 +               */
 +              _updateLabel: function () {
 +                      this._languageInput.find('.dropdownToggle > span').text(this._availableLanguages[this._languageID]);
 +              },
 +              
 +              /**
 +               * Sets language id upon submit.
 +               */
 +              _submit: function () {
 +                      this._languageInput.next('input[name=languageID]').prop('value', this._languageID);
 +              }
 +      });
 +      
 +      /**
 +       * Loads smiley categories upon user request.
 +       */
 +      WCF.Message.SmileyCategories = Class.extend({
 +              /**
 +               * list of already loaded category ids
 +               * @var        array<integer>
 +               */
 +              _cache: [],
 +              
 +              /**
 +               * action proxy
 +               * @var        WCF.Action.Proxy
 +               */
 +              _proxy: null,
 +              
 +              /**
 +               * wysiwyg editor selector
 +               * @var        string
 +               */
 +              _wysiwygSelector: '',
 +              
 +              /**
 +               * Initializes the smiley loader.
 +               *
 +               * @param        string                wysiwygSelector
 +               */
 +              init: function (wysiwygSelector) {
 +                      this._proxy = new WCF.Action.Proxy({
 +                              success: $.proxy(this._success, this)
 +                      });
 +                      this._wysiwygSelector = wysiwygSelector;
 +                      
 +                      $('#smilies-' + this._wysiwygSelector).on('messagetabmenushow', $.proxy(this._click, this));
 +              },
 +              
 +              /**
 +               * Handles tab menu clicks.
 +               *
 +               * @param        {Event}        event
 +               * @param        {Object}        data
 +               */
 +              _click: function (event, data) {
 +                      event.preventDefault();
 +                      
 +                      var $categoryID = parseInt(data.activeTab.tab.data('smileyCategoryID'));
 +                      
 +                      // ignore global category, will always be pre-loaded
 +                      if (!$categoryID) {
 +                              return;
 +                      }
 +                      
 +                      // smilies have already been loaded for this tab, ignore
 +                      if (data.activeTab.container.children('ul.smileyList').length) {
 +                              return;
 +                      }
 +                      
 +                      // cache exists
 +                      if (this._cache[$categoryID] !== undefined) {
 +                              data.activeTab.container.html(this._cache[$categoryID]);
 +                              return;
 +                      }
 +                      
 +                      // load content
 +                      this._proxy.setOption('data', {
 +                              actionName: 'getSmilies',
 +                              className: 'wcf\\data\\smiley\\category\\SmileyCategoryAction',
 +                              objectIDs: [$categoryID]
 +                      });
 +                      this._proxy.sendRequest();
 +              },
 +              
 +              /**
 +               * Handles successful AJAX requests.
 +               *
 +               * @param        object                data
 +               * @param        string                textStatus
 +               * @param        jQuery                jqXHR
 +               */
 +              _success: function (data, textStatus, jqXHR) {
 +                      var $categoryID = parseInt(data.returnValues.smileyCategoryID);
 +                      this._cache[$categoryID] = data.returnValues.template;
 +                      
 +                      $('#smilies-' + this._wysiwygSelector + '-' + $categoryID).html(data.returnValues.template);
                }
 -              
 -              this._super('wcf\\data\\bbcode\\MessagePreviewAction', this._options.messageFieldID, this._options.previewButtonID);
 -      },
 -      
 -      /**
 -       * @see WCF.Message.Preview._handleResponse()
 -       */
 -      _handleResponse: function(data) {
 -              require(['WoltLabSuite/Core/Ui/Dialog'], (function(UiDialog) {
 -                      UiDialog.open(this, '<div class="htmlContent">' + data.returnValues.message + '</div>');
 -              }).bind(this));
 -      },
 +      });
        
        /**
 -       * @see WCF.Message.Preview._getParameters()
 +       * Handles smiley clicks.
 +       *
 +       * @param        string                wysiwygSelector
         */
 -      _getParameters: function(message) {
 -              var $parameters = this._super(message);
 +      WCF.Message.Smilies = Class.extend({
 +              /**
 +               * wysiwyg editor id
 +               * @var        string
 +               */
 +              _editorId: '',
                
 -              for (var key in this._options) {
 -                      if (this._options.hasOwnProperty(key) && key !== 'messageFieldID' && key !== 'previewButtonID') {
 -                              $parameters[key] = this._options[key];
 +              /**
 +               * Initializes the smiley handler.
 +               *
 +               * @param        {string}        editorId
 +               */
 +              init: function (editorId) {
 +                      this._editorId = editorId;
 +                      
 +                      $('.messageTabMenu[data-wysiwyg-container-id=' + this._editorId + ']').on('mousedown', '.jsSmiley', this._smileyClick.bind(this));
 +              },
 +              
 +              /**
 +               * Handles tab smiley clicks.
 +               *
 +               * @param        {Event}                event
 +               */
 +              _smileyClick: function (event) {
 +                      event.preventDefault();
 +                      
 +                      require(['EventHandler'], (function (EventHandler) {
 +                              EventHandler.fire('com.woltlab.wcf.redactor2', 'insertSmiley_' + this._editorId, {
 +                                      img: event.currentTarget.children[0]
 +                              });
 +                      }).bind(this));
 +              }
 +      });
 +      
 +      /**
 +       * Provides an inline message editor.
 +       *
 +       * @deprecated        3.0 - please use `WoltLabSuite/Core/Ui/Message/InlineEditor` instead
 +       *
 +       * @param        integer                containerID
 +       */
 +      WCF.Message.InlineEditor = Class.extend({
 +              /**
 +               * list of messages
 +               * @var        object
 +               */
 +              _container: {},
 +              
 +              /**
 +               * container id
 +               * @var        int
 +               */
 +              _containerID: 0,
 +              
 +              /**
 +               * list of dropdowns
 +               * @var        object
 +               */
 +              _dropdowns: {},
 +              
 +              /**
 +               * CSS selector for the message container
 +               * @var        string
 +               */
 +              _messageContainerSelector: '.jsMessage',
 +              
 +              /**
 +               * prefix of the message editor CSS id
 +               * @var        string
 +               */
 +              _messageEditorIDPrefix: 'messageEditor',
 +              
 +              /**
 +               * Initializes a new WCF.Message.InlineEditor object.
 +               *
 +               * @param        integer                                containerID
 +               * @param        boolean                                supportExtendedForm
 +               * @param        WCF.Message.Quote.Manager        quoteManager
 +               */
 +              init: function (containerID, supportExtendedForm, quoteManager) {
 +                      require(['WoltLabSuite/Core/Ui/Message/InlineEditor'], (function (UiMessageInlineEditor) {
 +                              new UiMessageInlineEditor({
 +                                      className: this._getClassName(),
 +                                      containerId: containerID,
 +                                      editorPrefix: this._messageEditorIDPrefix,
 +                                      
 +                                      messageSelector: this._messageContainerSelector,
 +                                      quoteManager: quoteManager || null,
 +                                      
 +                                      callbackDropdownInit: this._callbackDropdownInit.bind(this)
 +                              });
 +                      }).bind(this));
 +              },
 +              
 +              /**
 +               * Loads WYSIWYG editor for selected message.
 +               *
 +               * @param        object                event
 +               * @param        integer                containerID
 +               * @return        boolean
 +               */
 +              _click: function (event, containerID) {
 +                      containerID = (event === null) ? ~~containerID : ~~elData(event.currentTarget, 'container-id');
 +                      
 +                      require(['WoltLabSuite/Core/Ui/Message/InlineEditor'], (function (UiMessageInlineEditor) {
 +                              UiMessageInlineEditor.legacyEdit(containerID);
 +                      }).bind(this));
 +                      
 +                      if (event) {
 +                              event.preventDefault();
                        }
 -              }
 -              
 -              return $parameters;
 -      },
 -      
 -      _dialogSetup: function() {
 -              return {
 -                      id: 'messagePreview',
 -                      options: {
 -                              title: WCF.Language.get('wcf.global.preview')
 -                      },
 -                      source: null
 -              }
 -      }
 -});
 +              },
 +              
 +              /**
 +               * Initializes the inline edit dropdown menu.
 +               *
 +               * @param        integer                containerID
 +               * @param        jQuery                dropdownMenu
 +               */
 +              _initDropdownMenu: function (containerID, dropdownMenu) {
 +              },
 +              
 +              _callbackDropdownInit: function (element, dropdownMenu) {
 +                      this._initDropdownMenu($(element).wcfIdentify(), $(dropdownMenu));
 +                      
 +                      return null;
 +              },
 +              
 +              /**
 +               * Returns message action class name.
 +               *
 +               * @return        string
 +               */
 +              _getClassName: function () {
 +                      return '';
 +              }
 +      });
 +      
 +      /**
 +       * Handles submit buttons for forms with an embedded WYSIWYG editor.
 +       */
 +      WCF.Message.Submit = {
 +              /**
 +               * list of registered buttons
 +               * @var        object
 +               */
 +              _buttons: {},
 +              
 +              /**
 +               * Registers submit button for specified wysiwyg container id.
 +               *
 +               * @param        string                wysiwygContainerID
 +               * @param        string                selector
 +               */
 +              registerButton: function (wysiwygContainerID, selector) {
 +                      if (!WCF.Browser.isChrome()) {
 +                              return;
 +                      }
 +                      
 +                      this._buttons[wysiwygContainerID] = $(selector);
 +              },
 +              
 +              /**
 +               * Triggers 'click' event for registered buttons.
 +               */
 +              execute: function (wysiwygContainerID) {
 +                      if (!this._buttons[wysiwygContainerID]) {
 +                              return;
 +                      }
 +                      
 +                      this._buttons[wysiwygContainerID].trigger('click');
 +              }
 +      };
 +}
 +else {
 +      WCF.Message.Preview = Class.extend({
 +              _className: "",
 +              _messageFieldID: "",
 +              _messageField: {},
 +              _proxy: {},
 +              _previewButton: {},
 +              _previewButtonLabel: "",
 +              init: function() {},
 +              _click: function() {},
 +              _getParameters: function() {},
 +              _getMessage: function() {},
 +              _success: function() {},
 +              _handleResponse: function() {},
 +              _failure: function() {}
 +      });
 +      
 +      WCF.Message.DefaultPreview = WCF.Message.Preview.extend({
 +              _dialog: {},
 +              _options: {},
 +              init: function() {},
 +              _handleResponse: function() {},
 +              _getParameters: function() {},
 +              _dialogSetup: function() {},
 +              _className: "",
 +              _messageFieldID: "",
 +              _messageField: {},
 +              _proxy: {},
 +              _previewButton: {},
 +              _previewButtonLabel: "",
 +              _click: function() {},
 +              _getMessage: function() {},
 +              _success: function() {},
 +              _failure: function() {}
 +      });
 +      
 +      WCF.Message.Multilingualism = Class.extend({
 +              _availableLanguages: {},
 +              _languageID: 0,
 +              _languageInput: {},
 +              init: function() {},
 +              _click: function() {},
 +              _disable: function() {},
 +              _updateLabel: function() {},
 +              _submit: function() {}
 +      });
 +      
 +      WCF.Message.SmileyCategories = Class.extend({
 +              _cache: {},
 +              _proxy: {},
 +              _wysiwygSelector: "",
 +              init: function() {},
 +              _click: function() {},
 +              _success: function() {}
 +      });
 +      
 +      WCF.Message.Smilies = Class.extend({
 +              _editorId: "",
 +              init: function() {},
 +              _smileyClick: function() {}
 +      });
 +      
 +      WCF.Message.InlineEditor = Class.extend({
 +              _container: {},
 +              _containerID: 0,
 +              _dropdowns: {},
 +              _messageContainerSelector: "",
 +              _messageEditorIDPrefix: "",
 +              init: function() {},
 +              _click: function() {},
 +              _initDropdownMenu: function() {},
 +              _callbackDropdownInit: function() {},
 +              _getClassName: function() {}
 +      });
 +      
 +      WCF.Message.Submit = {
 +              _buttons: {},
 +              registerButton: function() {},
 +              execute: function() {}
 +      };
 +}
  
  /**
 - * Handles multilingualism for messages.
 - * 
 - * @param     integer         languageID
 - * @param     object          availableLanguages
 - * @param     boolean         forceSelection
 + * Namespace for message quotes.
   */
 -WCF.Message.Multilingualism = Class.extend({
 -      /**
 -       * list of available languages
 -       * @var object
 -       */
 -      _availableLanguages: { },
 -      
 -      /**
 -       * language id
 -       * @var integer
 -       */
 -      _languageID: 0,
 -      
 -      /**
 -       * language input element
 -       * @var jQuery
 -       */
 -      _languageInput: null,
 -      
 -      /**
 -       * Initializes WCF.Message.Multilingualism
 -       * 
 -       * @param       integer         languageID
 -       * @param       object          availableLanguages
 -       * @param       boolean         forceSelection
 -       */
 -      init: function(languageID, availableLanguages, forceSelection) {
 -              this._availableLanguages = availableLanguages;
 -              this._languageID = languageID || 0;
 -              
 -              this._languageInput = $('#languageID');
 -              
 -              // preselect current language id
 -              this._updateLabel();
 -              
 -              // register event listener
 -              this._languageInput.find('.dropdownMenu > li').click($.proxy(this._click, this));
 -              
 -              // add element to disable multilingualism
 -              if (!forceSelection) {
 -                      var $dropdownMenu = this._languageInput.find('.dropdownMenu');
 -                      $('<li class="dropdownDivider" />').appendTo($dropdownMenu);
 -                      $('<li><span><span class="badge">' + this._availableLanguages[0] + '</span></span></li>').click($.proxy(this._disable, this)).appendTo($dropdownMenu);
 -              }
 -              
 -              // bind submit event
 -              this._languageInput.parents('form').submit($.proxy(this._submit, this));
 -      },
 -      
 -      /**
 -       * Handles language selections.
 -       * 
 -       * @param       object          event
 -       */
 -      _click: function(event) {
 -              this._languageID = $(event.currentTarget).data('languageID');
 -              this._updateLabel();
 -      },
 -      
 -      /**
 -       * Disables language selection.
 -       */
 -      _disable: function() {
 -              this._languageID = 0;
 -              this._updateLabel();
 -      },
 -      
 -      /**
 -       * Updates selected language.
 -       */
 -      _updateLabel: function() {
 -              this._languageInput.find('.dropdownToggle > span').text(this._availableLanguages[this._languageID]);
 -      },
 -      
 -      /**
 -       * Sets language id upon submit.
 -       */
 -      _submit: function() {
 -              this._languageInput.next('input[name=languageID]').prop('value', this._languageID);
 -      }
 -});
 -
 -/**
 - * Loads smiley categories upon user request.
 - */
 -WCF.Message.SmileyCategories = Class.extend({
 -      /**
 -       * list of already loaded category ids
 -       * @var array<integer>
 -       */
 -      _cache: [ ],
 -      
 -      /**
 -       * action proxy
 -       * @var WCF.Action.Proxy
 -       */
 -      _proxy: null,
 -      
 -      /**
 -       * wysiwyg editor selector
 -       * @var string
 -       */
 -      _wysiwygSelector: '',
 -      
 -      /**
 -       * Initializes the smiley loader.
 -       * 
 -       * @param       string          wysiwygSelector
 -       */
 -      init: function(wysiwygSelector) {
 -              this._proxy = new WCF.Action.Proxy({
 -                      success: $.proxy(this._success, this)
 -              });
 -              this._wysiwygSelector = wysiwygSelector;
 -              
 -              $('#smilies-' + this._wysiwygSelector).on('messagetabmenushow', $.proxy(this._click, this));
 -      },
 -      
 -      /**
 -       * Handles tab menu clicks.
 -       * 
 -       * @param       {Event}         event
 -       * @param       {Object}        data
 -       */
 -      _click: function(event, data) {
 -              event.preventDefault();
 -              
 -              var $categoryID = parseInt(data.activeTab.tab.data('smileyCategoryID'));
 -              
 -              // ignore global category, will always be pre-loaded
 -              if (!$categoryID) {
 -                      return;
 -              }
 -              
 -              // smilies have already been loaded for this tab, ignore
 -              if (data.activeTab.container.children('ul.smileyList').length) {
 -                      return;
 -              }
 -              
 -              // cache exists
 -              if (this._cache[$categoryID] !== undefined) {
 -                      data.activeTab.container.html(this._cache[$categoryID]);
 -                      return;
 -              }
 -              
 -              // load content
 -              this._proxy.setOption('data', {
 -                      actionName: 'getSmilies',
 -                      className: 'wcf\\data\\smiley\\category\\SmileyCategoryAction',
 -                      objectIDs: [ $categoryID ]
 -              });
 -              this._proxy.sendRequest();
 -      },
 -      
 -      /**
 -       * Handles successful AJAX requests.
 -       * 
 -       * @param       object          data
 -       * @param       string          textStatus
 -       * @param       jQuery          jqXHR
 -       */
 -      _success: function(data, textStatus, jqXHR) {
 -              var $categoryID = parseInt(data.returnValues.smileyCategoryID);
 -              this._cache[$categoryID] = data.returnValues.template;
 -              
 -              $('#smilies-' + this._wysiwygSelector + '-' + $categoryID).html(data.returnValues.template);
 -      }
 -});
 +WCF.Message.Quote = { };
  
 -/**
 - * Handles smiley clicks.
 - * 
 - * @param     string          wysiwygSelector
 - */
 -WCF.Message.Smilies = Class.extend({
 -      /**
 -       * wysiwyg editor id
 -       * @var string
 -       */
 -      _editorId: '',
 -      
 -      /**
 -       * Initializes the smiley handler.
 -       * 
 -       * @param       {string}        editorId
 -       */
 -      init: function(editorId) {
 -              this._editorId = editorId;
 -              
 -              $('.messageTabMenu[data-wysiwyg-container-id=' + this._editorId + ']').on('mousedown', '.jsSmiley', this._smileyClick.bind(this));
 -      },
 -      
 -      /**
 -       * Handles tab smiley clicks.
 -       * 
 -       * @param       {Event}         event
 -       */
 -      _smileyClick: function(event) {
 -              event.preventDefault();
 -              
 -              require(['EventHandler'], (function(EventHandler) {
 -                      EventHandler.fire('com.woltlab.wcf.redactor2', 'insertSmiley_' + this._editorId, {
 -                              img: event.currentTarget.children[0]
 +if (COMPILER_TARGET_DEFAULT) {
 +      /**
 +       * Handles message quotes.
 +       */
 +      WCF.Message.Quote.Handler = Class.extend({
 +              /**
 +               * active container id
 +               * @var        string
 +               */
 +              _activeContainerID: '',
 +              
 +              /**
 +               * action class name
 +               * @var        string
 +               */
 +              _className: '',
 +              
 +              /**
 +               * list of message containers
 +               * @var        object
 +               */
 +              _containers: {},
 +              
 +              /**
 +               * container selector
 +               * @var        string
 +               */
 +              _containerSelector: '',
 +              
 +              /**
 +               * 'copy quote' overlay
 +               * @var        jQuery
 +               */
 +              _copyQuote: null,
 +              
 +              /**
 +               * marked message
 +               * @var        string
 +               */
 +              _message: '',
 +              
 +              /**
 +               * message body selector
 +               * @var        string
 +               */
 +              _messageBodySelector: '',
 +              
 +              /**
 +               * object id
 +               * @var        {int}
 +               */
 +              _objectID: 0,
 +              
 +              /**
 +               * object type name
 +               * @var        string
 +               */
 +              _objectType: '',
 +              
 +              /**
 +               * action proxy
 +               * @var        WCF.Action.Proxy
 +               */
 +              _proxy: null,
 +              
 +              /**
 +               * quote manager
 +               * @var        WCF.Message.Quote.Manager
 +               */
 +              _quoteManager: null,
 +              
 +              /**
 +               * Initializes the quote handler for given object type.
 +               *
 +               * @param        {WCF.Message.Quote.Manager}        quoteManager
 +               * @param        {string}                        className
 +               * @param        {string}                        objectType
 +               * @param        {string}                        containerSelector
 +               * @param        {string}                        messageBodySelector
 +               * @param        {string}                        messageContentSelector
 +               * @param        {boolean}                        supportDirectInsert
 +               */
 +              init: function (quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) {
 +                      this._className = className;
 +                      if (this._className === '') {
 +                              console.debug("[WCF.Message.QuoteManager] Empty class name given, aborting.");
 +                              return;
 +                      }
 +                      
 +                      this._objectType = objectType;
 +                      if (this._objectType === '') {
 +                              console.debug("[WCF.Message.QuoteManager] Empty object type name given, aborting.");
 +                              return;
 +                      }
 +                      
 +                      this._containerSelector = containerSelector;
 +                      this._message = '';
 +                      this._messageBodySelector = messageBodySelector;
 +                      this._objectID = 0;
 +                      this._proxy = new WCF.Action.Proxy({
 +                              success: $.proxy(this._success, this)
                        });
 -              }).bind(this));
 -      }
 -});
 -
 -/**
 - * Provides an inline message editor.
 - * 
 - * @deprecated        3.0 - please use `WoltLabSuite/Core/Ui/Message/InlineEditor` instead
 - * 
 - * @param     integer         containerID
 - */
 -WCF.Message.InlineEditor = Class.extend({
 -      /**
 -       * list of messages
 -       * @var object
 -       */
 -      _container: { },
 -      
 -      /**
 -       * container id
 -       * @var int
 -       */
 -      _containerID: 0,
 -      
 -      /**
 -       * list of dropdowns
 -       * @var object
 -       */
 -      _dropdowns: { },
 -      
 -      /**
 -       * CSS selector for the message container
 -       * @var string
 -       */
 -      _messageContainerSelector: '.jsMessage',
 -      
 -      /**
 -       * prefix of the message editor CSS id
 -       * @var string
 -       */
 -      _messageEditorIDPrefix: 'messageEditor',
 -      
 -      /**
 -       * Initializes a new WCF.Message.InlineEditor object.
 -       * 
 -       * @param       integer                         containerID
 -       * @param       boolean                         supportExtendedForm
 -       * @param       WCF.Message.Quote.Manager       quoteManager
 -       */
 -      init: function(containerID, supportExtendedForm, quoteManager) {
 -              require(['WoltLabSuite/Core/Ui/Message/InlineEditor'], (function(UiMessageInlineEditor) {
 -                      new UiMessageInlineEditor({
 -                              className: this._getClassName(),
 -                              containerId: containerID,
 -                              editorPrefix: this._messageEditorIDPrefix,
 -                              
 -                              messageSelector: this._messageContainerSelector,
 +                      
 +                      this._initContainers();
 +                      
 +                      supportDirectInsert = (supportDirectInsert && quoteManager.supportPaste());
 +                      this._initCopyQuote(supportDirectInsert);
 +                      
 +                      $(document).mouseup($.proxy(this._mouseUp, this));
 +                      
 +                      // register with quote manager
 +                      this._quoteManager = quoteManager;
 +                      this._quoteManager.register(this._objectType, this);
 +                      
 +                      // register with DOMNodeInsertedHandler
 +                      WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.Quote.Handler' + objectType.hashCode(), $.proxy(this._initContainers, this));
 +              },
 +              
 +              /**
 +               * Initializes message containers.
 +               */
 +              _initContainers: function () {
 +                      var self = this;
 +                      $(this._containerSelector).each(function (index, container) {
 +                              var $container = $(container);
 +                              var $containerID = $container.wcfIdentify();
                                
 -                              callbackDropdownInit: this._callbackDropdownInit.bind(this)
 -                      });
 -              }).bind(this));
 -      },
 -      
 -      /**
 -       * Loads WYSIWYG editor for selected message.
 -       * 
 -       * @param       object          event
 -       * @param       integer         containerID
 -       * @return      boolean
 -       */
 -      _click: function(event, containerID) {
 -              containerID = (event === null) ? ~~containerID : ~~elData(event.currentTarget, 'container-id');
 -              
 -              require(['WoltLabSuite/Core/Ui/Message/InlineEditor'], (function(UiMessageInlineEditor) {
 -                      UiMessageInlineEditor.legacyEdit(containerID);
 -              }).bind(this));
 -              
 -              if (event) {
 -                      event.preventDefault();
 -              }
 -      },
 -      
 -      /**
 -       * Initializes the inline edit dropdown menu.
 -       * 
 -       * @param       integer         containerID
 -       * @param       jQuery          dropdownMenu
 -       */
 -      _initDropdownMenu: function(containerID, dropdownMenu) { },
 -      
 -      _callbackDropdownInit: function(element, dropdownMenu) {
 -              this._initDropdownMenu($(element).wcfIdentify(), $(dropdownMenu));
 -              
 -              return null;
 -      },
 -      
 -      /**
 -       * Returns message action class name.
 -       * 
 -       * @return      string
 -       */
 -      _getClassName: function() {
 -              return '';
 -      }
 -});
 -
 -/**
 - * Handles submit buttons for forms with an embedded WYSIWYG editor.
 - */
 -WCF.Message.Submit = {
 -      /**
 -       * list of registered buttons
 -       * @var object
 -       */
 -      _buttons: { },
 -      
 -      /**
 -       * Registers submit button for specified wysiwyg container id.
 -       * 
 -       * @param       string          wysiwygContainerID
 -       * @param       string          selector
 -       */
 -      registerButton: function(wysiwygContainerID, selector) {
 -              if (!WCF.Browser.isChrome()) {
 -                      return;
 -              }
 -              
 -              this._buttons[wysiwygContainerID] = $(selector);
 -      },
 -      
 -      /**
 -       * Triggers 'click' event for registered buttons.
 -       */
 -      execute: function(wysiwygContainerID) {
 -              if (!this._buttons[wysiwygContainerID]) {
 -                      return;
 -              }
 -              
 -              this._buttons[wysiwygContainerID].trigger('click');
 -      }
 -};
 -
 -/**
 - * Namespace for message quotes.
 - */
 -WCF.Message.Quote = { };
 -
 -/**
 - * Handles message quotes.
 - */
 -WCF.Message.Quote.Handler = Class.extend({
 -      /**
 -       * active container id
 -       * @var string
 -       */
 -      _activeContainerID: '',
 -      
 -      /**
 -       * action class name
 -       * @var string
 -       */
 -      _className: '',
 -      
 -      /**
 -       * list of message containers
 -       * @var object
 -       */
 -      _containers: { },
 -      
 -      /**
 -       * container selector
 -       * @var string
 -       */
 -      _containerSelector: '',
 -      
 -      /**
 -       * 'copy quote' overlay
 -       * @var jQuery
 -       */
 -      _copyQuote: null,
 -      
 -      /**
 -       * marked message
 -       * @var string
 -       */
 -      _message: '',
 -      
 -      /**
 -       * message body selector
 -       * @var string
 -       */
 -      _messageBodySelector: '',
 -      
 -      /**
 -       * object id
 -       * @var integer
 -       */
 -      _objectID: 0,
 -      
 -      /**
 -       * object type name
 -       * @var string
 -       */
 -      _objectType: '',
 -      
 -      /**
 -       * action proxy
 -       * @var WCF.Action.Proxy
 -       */
 -      _proxy: null,
 -      
 -      /**
 -       * quote manager
 -       * @var WCF.Message.Quote.Manager
 -       */
 -      _quoteManager: null,
 -      
 -      /**
 -       * Initializes the quote handler for given object type.
 -       * 
 -       * @param       {WCF.Message.Quote.Manager}     quoteManager
 -       * @param       {string}                        className
 -       * @param       {string}                        objectType
 -       * @param       {string}                        containerSelector
 -       * @param       {string}                        messageBodySelector
 -       * @param       {string}                        messageContentSelector
 -       * @param       {boolean}                       supportDirectInsert
 -       */
 -      init: function(quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) {
 -              this._className = className;
 -              if (this._className == '') {
 -                      console.debug("[WCF.Message.QuoteManager] Empty class name given, aborting.");
 -                      return;
 -              }
 -              
 -              this._objectType = objectType;
 -              if (this._objectType == '') {
 -                      console.debug("[WCF.Message.QuoteManager] Empty object type name given, aborting.");
 -                      return;
 -              }
 -              
 -              this._containerSelector = containerSelector;
 -              this._message = '';
 -              this._messageBodySelector = messageBodySelector;
 -              this._messageContentSelector = messageContentSelector;
 -              this._objectID = 0;
 -              this._proxy = new WCF.Action.Proxy({
 -                      success: $.proxy(this._success, this)
 -              });
 -              
 -              this._initContainers();
 -              
 -              supportDirectInsert = (supportDirectInsert && quoteManager.supportPaste()) ? true : false;
 -              this._initCopyQuote(supportDirectInsert);
 -              
 -              $(document).mouseup($.proxy(this._mouseUp, this));
 -              
 -              // register with quote manager
 -              this._quoteManager = quoteManager;
 -              this._quoteManager.register(this._objectType, this);
 -              
 -              // register with DOMNodeInsertedHandler
 -              WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.Quote.Handler' + objectType.hashCode(), $.proxy(this._initContainers, this));
 -      },
 -      
 -      /**
 -       * Initializes message containers.
 -       */
 -      _initContainers: function() {
 -              var self = this;
 -              $(this._containerSelector).each(function(index, container) {
 -                      var $container = $(container);
 -                      var $containerID = $container.wcfIdentify();
 -                      
 -                      if (!self._containers[$containerID]) {
 -                              self._containers[$containerID] = $container;
 -                              if ($container.hasClass('jsInvalidQuoteTarget')) {
 -                                      return true;
 +                              if (!self._containers[$containerID]) {
 +                                      self._containers[$containerID] = $container;
 +                                      if ($container.hasClass('jsInvalidQuoteTarget')) {
 +                                              return true;
 +                                      }
 +                                      
 +                                      if (self._messageBodySelector) {
 +                                              $container.data('body', $container.find(self._messageBodySelector).data('containerID', $containerID));
 +                                      }
 +                                      
 +                                      $container.mousedown($.proxy(self._mouseDown, self));
 +                                      
 +                                      // bind event to quote whole message
 +                                      self._containers[$containerID].find('.jsQuoteMessage').click($.proxy(self._saveFullQuote, self));
                                }
 -                              
 -                              if (self._messageBodySelector) {
 -                                      $container.data('body', $container.find(self._messageBodySelector).data('containerID', $containerID));
 +                      });
 +              },
 +              
 +              /**
 +               * Handles mouse down event.
 +               *
 +               * @param        {Event}                event
 +               */
 +              _mouseDown: function (event) {
 +                      // hide copy quote
 +                      this._copyQuote.removeClass('active');
 +                      
 +                      this._activeContainerID = (event.currentTarget.classList.contains('jsInvalidQuoteTarget')) ? '' : event.currentTarget.id;
 +              },
 +              
 +              /**
 +               * Returns the text of a node and its children.
 +               *
 +               * @param        {Node}                node
 +               * @return        {string}
 +               */
 +              _getNodeText: function (node) {
 +                      // work-around for IE, see http://stackoverflow.com/a/5983176
 +                      var $nodeFilter = function (node) {
 +                              switch (node.tagName) {
 +                                      case 'BLOCKQUOTE':
 +                                      case 'SCRIPT':
 +                                              return NodeFilter.FILTER_REJECT;
 +                                      
 +                                      case 'IMG':
 +                                              if (!node.classList.contains('smiley') || node.alt.length === 0) {
 +                                                      return NodeFilter.FILTER_REJECT;
 +                                              }
 +                                              // fallthrough
 +                                      
 +                                      //noinspection FallthroughInSwitchStatementJS
 +                                      default:
 +                                              return NodeFilter.FILTER_ACCEPT;
                                }
 +                      };
 +                      $nodeFilter.acceptNode = $nodeFilter;
 +                      
 +                      var $walker = document.createTreeWalker(
 +                              node,
 +                              NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
 +                              $nodeFilter,
 +                              true
 +                      );
 +                      
 +                      var $text = '', ignoreLinks = [], value;
 +                      while ($walker.nextNode()) {
 +                              var $node = $walker.currentNode;
                                
 -                              $container.mousedown($.proxy(self._mouseDown, self));
 -                              
 -                              // bind event to quote whole message
 -                              self._containers[$containerID].find('.jsQuoteMessage').click($.proxy(self._saveFullQuote, self));
 -                      }
 -              });
 -      },
 -      
 -      /**
 -       * Handles mouse down event.
 -       * 
 -       * @param       {Event}         event
 -       */
 -      _mouseDown: function(event) {
 -              // hide copy quote
 -              this._copyQuote.removeClass('active');
 -              
 -              this._activeContainerID = (event.currentTarget.classList.contains('jsInvalidQuoteTarget')) ? '' : event.currentTarget.id;
 -      },
 -      
 -      /**
 -       * Returns the text of a node and its children.
 -       * 
 -       * @param       {Node}          node
 -       * @return      {string}
 -       */
 -      _getNodeText: function(node) {
 -              // work-around for IE, see http://stackoverflow.com/a/5983176
 -              var $nodeFilter = function(node) {
 -                      switch (node.tagName) {
 -                              case 'BLOCKQUOTE':
 -                              case 'IMG':
 -                              case 'SCRIPT':
 -                                      return NodeFilter.FILTER_REJECT;
 -                              break;
 -                              
 -                              default:
 -                                      return NodeFilter.FILTER_ACCEPT;
 -                              break;
 -                      }
 -              };
 -              $nodeFilter.acceptNode = $nodeFilter;
 -              
 -              var $walker = document.createTreeWalker(
 -                      node,
 -                      NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
 -                      $nodeFilter,
 -                      true
 -              );
 -              
 -              var $text = '';
 -              while ($walker.nextNode()) {
 -                      var $node = $walker.currentNode;
 -                      
 -                      if ($node.nodeType === Node.ELEMENT_NODE) {
 -                              switch ($node.tagName) {
 -                                      case 'BR':
 -                                      case 'LI':
 -                                      case 'UL':
 -                                              $text += "\n";
 -                                              break;
 -                                      
 -                                      case 'TD':
 -                                              if (!$.browser.msie) {
 +                              if ($node.nodeType === Node.ELEMENT_NODE) {
 +                                      switch ($node.tagName) {
 +                                              case 'A':
 +                                                      // \u2026 === &hellip;
 +                                                      value = $node.textContent;
 +                                                      if (value.indexOf('\u2026') > 0) {
 +                                                              var tmp = value.split(/\u2026/);
 +                                                              if (tmp.length === 2) {
 +                                                                      var href = $node.href;
 +                                                                      if (href.indexOf(tmp[0]) === 0 && href.substr(tmp[1].length * -1) === tmp[1]) {
 +                                                                              // truncated url, use original href to preserve link
 +                                                                              $text += href;
 +                                                                              ignoreLinks.push($node);
 +                                                                      }
 +                                                              }
 +                                                      }
 +                                                      break;
 +                                              
 +                                              case 'BR':
 +                                              case 'LI':
 +                                              case 'UL':
                                                        $text += "\n";
 -                                              }
 -                                              break;
 +                                                      break;
 +                                              
 +                                              case 'TD':
 +                                                      if (!$.browser.msie) {
 +                                                              $text += "\n";
 +                                                      }
 +                                                      break;
 +                                              
 +                                              case 'P':
 +                                                      $text += "\n\n";
 +                                                      break;
 +                                                      
 +                                              // smilies
 +                                              case 'IMG':
 +                                                      $text += " " + $node.alt + " ";
 +                                                      break;
 +                                      }
 +                              }
 +                              else {
 +                                      if ($node.parentNode.nodeName === 'A' && ignoreLinks.indexOf($node.parentNode) !== -1) {
 +                                              // ignore text content of links that have already been captured
 +                                              continue;
 +                                      }
                                        
 -                                      case 'P':
 -                                              $text += "\n\n";
 -                                              break;
 +                                      $text += $node.nodeValue.replace(/\n/g, '');
                                }
 +                              
                        }
 -                      else {
 -                              $text += $node.nodeValue.replace(/\n/g, '');
 +                      
 +                      return $text;
 +              },
 +              
 +              /**
 +               * Handles the mouse up event.
 +               */
 +              _mouseUp: function () {
 +                      // ignore event
 +                      if (this._activeContainerID === '') {
 +                              this._copyQuote.removeClass('active');
 +                              return;
                        }
                        
 -              }
 -              
 -              return $text;
 -      },
 -      
 -      /**
 -       * Handles the mouse up event.
 -       */
 -      _mouseUp: function() {
 -              // ignore event
 -              if (this._activeContainerID === '') {
 -                      this._copyQuote.removeClass('active');
 -                      return;
 -              }
 -              
 -              var selection = window.getSelection();
 -              if (selection.rangeCount !== 1 || selection.isCollapsed) {
 -                      this._copyQuote.removeClass('active');
 -                      return;
 -              }
 -              
 -              var $container = this._containers[this._activeContainerID];
 -              var $objectID = $container.data('objectID');
 -              $container = $container.data('body') || $container;
 -              
 -              var anchorNode = selection.anchorNode;
 -              while (anchorNode) {
 -                      if (anchorNode === $container[0]) {
 -                              break;
 +                      var selection = window.getSelection();
 +                      if (selection.rangeCount !== 1 || selection.isCollapsed) {
 +                              this._copyQuote.removeClass('active');
 +                              return;
                        }
                        
 -                      anchorNode = anchorNode.parentNode;
 -              }
 -              
 -              // selection spans unrelated nodes
 -              if (anchorNode !== $container[0]) {
 -                      this._copyQuote.removeClass('active');
 -                      return;
 -              }
 -              
 -              var $selection = this._getSelectedText();
 -              var $text = $.trim($selection);
 -              if ($text == '') {
 -                      this._copyQuote.removeClass('active');
 +                      var $container = this._containers[this._activeContainerID];
 +                      var $objectID = $container.data('objectID');
 +                      $container = $container.data('body') || $container;
                        
 -                      return;
 -              }
 -              
 -              // check if mousedown/mouseup took place inside a blockquote
 -              var range = selection.getRangeAt(0);
 -              var startContainer = (range.startContainer.nodeType === Node.TEXT_NODE) ? range.startContainer.parentNode : range.startContainer;
 -              var endContainer = (range.endContainer.nodeType === Node.TEXT_NODE) ? range.endContainer.parentNode : range.endContainer;
 -              if (startContainer.closest('blockquote') || endContainer.closest('blockquote')) {
 -                      this._copyQuote.removeClass('active');
 +                      var anchorNode = selection.anchorNode;
 +                      while (anchorNode) {
 +                              if (anchorNode === $container[0]) {
 +                                      break;
 +                              }
 +                              
 +                              anchorNode = anchorNode.parentNode;
 +                      }
                        
 -                      return;
 -              }
 -              
 -              // compare selection with message text of given container
 -              var $messageText = this._getNodeText($container[0]);
 -              
 -              // selected text is not part of $messageText or contains text from unrelated nodes
 -              if (this._normalize($messageText).indexOf(this._normalize($text)) === -1) {
 -                      return;
 -              }
 -              this._copyQuote.addClass('active');
 -              
 -              var $coordinates = this._getBoundingRectangle($container, window.getSelection());
 -              var $dimensions = this._copyQuote.getDimensions('outer');
 -              var $left = ($coordinates.right - $coordinates.left) / 2 - ($dimensions.width / 2) + $coordinates.left;
 -              
 -              this._copyQuote.css({
 -                      top: $coordinates.top - $dimensions.height - 7 + 'px',
 -                      left: $left + 'px'
 -              });
 -              this._copyQuote.removeClass('active');
 -              
 -              // reset containerID
 -              this._activeContainerID = '';
 -              
 -              // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
 -              var self = this;
 -              window.setTimeout(function() {
 -                      var $text = $.trim(self._getSelectedText());
 -                      if ($text != '') {
 -                              self._copyQuote.addClass('active');
 -                              self._message = $text;
 -                              self._objectID = $objectID;
 +                      // selection spans unrelated nodes
 +                      if (anchorNode !== $container[0]) {
 +                              this._copyQuote.removeClass('active');
 +                              return;
                        }
 -              }, 10);
 -      },
 -      
 -      /**
 -       * Normalizes a text for comparison.
 -       * 
 -       * @param       {string}        text
 -       * @return      {string}
 -       */
 -      _normalize: function(text) {
 -              return text.replace(/\r?\n|\r/g, "\n").replace(/\s/g, ' ').replace(/\s{2,}/g, ' ');
 -      },
 -      
 -      /**
 -       * Returns the offsets of the selection's bounding rectangle.
 -       * 
 -       * @return      {Object}
 -       */
 -      _getBoundingRectangle: function(container, selection) {
 -              var $coordinates = null;
 -              
 -              if (selection.rangeCount > 0) {
 -                      // the coordinates returned by getBoundingClientRect() are relative to the viewport, not the document!
 -                      var $rect = selection.getRangeAt(0).getBoundingClientRect();
                        
 -                      $coordinates = {
 -                              left: $rect.left,
 -                              right: $rect.right,
 -                              top: $rect.top + $(document).scrollTop()
 -                      };
 -              }
 -              
 -              return $coordinates;
 -      },
 -      
 -      /**
 -       * Initializes the 'copy quote' element.
 -       * 
 -       * @param       {boolean}       supportDirectInsert
 -       */
 -      _initCopyQuote: function(supportDirectInsert) {
 -              this._copyQuote = $('#quoteManagerCopy');
 -              if (!this._copyQuote.length) {
 -                      this._copyQuote = $('<div id="quoteManagerCopy" class="balloonTooltip interactive"><span class="jsQuoteManagerStore">' + WCF.Language.get('wcf.message.quote.quoteSelected') + '</span></div>').appendTo(document.body);
 -                      var $storeQuote = this._copyQuote.children('span.jsQuoteManagerStore').click($.proxy(this._saveQuote, this));
 -                      if (supportDirectInsert) {
 -                              $('<span class="jsQuoteManagerQuoteAndInsert">' + WCF.Language.get('wcf.message.quote.quoteAndReply') + '</span>').click($.proxy(this._saveAndInsertQuote, this)).insertAfter($storeQuote);
 +                      var $selection = this._getSelectedText();
 +                      var $text = $.trim($selection);
 +                      if ($text == '') {
 +                              this._copyQuote.removeClass('active');
 +                              
 +                              return;
                        }
 -              }
 -      },
 -      
 -      /**
 -       * Returns the text selection.
 -       * 
 -       * @return      string
 -       */
 -      _getSelectedText: function() {
 -              var $selection = window.getSelection();
 -              if ($selection.rangeCount) {
 -                      return this._getNodeText($selection.getRangeAt(0).cloneContents());
 -              }
 -              
 -              return '';
 -      },
 -      
 -      /**
 -       * Saves a full quote.
 -       * 
 -       * @param       {Event}         event
 -       */
 -      _saveFullQuote: function(event) {
 -              event.preventDefault();
 -              
 -              var $listItem = $(event.currentTarget);
 -              
 -              this._proxy.setOption('data', {
 -                      actionName: 'saveFullQuote',
 -                      className: this._className,
 -                      interfaceName: 'wcf\\data\\IMessageQuoteAction',
 -                      objectIDs: [ $listItem.data('objectID') ]
 -              });
 -              this._proxy.sendRequest();
 -              
 -              // mark element as quoted
 -              if ($listItem.data('isQuoted')) {
 -                      $listItem.data('isQuoted', false).children('a').removeClass('active');
 -              }
 -              else {
 -                      $listItem.data('isQuoted', true).children('a').addClass('active');
 -              }
 -              
 -              // close navigation on mobile
 -              var $navigationList = $listItem.parents('.buttonGroupNavigation');
 -              if ($navigationList.hasClass('jsMobileButtonGroupNavigation')) {
 -                      $navigationList.children('.dropdownLabel').trigger('click');
 -              }
 -      },
 -      
 -      /**
 -       * Saves a quote.
 -       * 
 -       * @param       {boolean}       renderQuote
 -       */
 -      _saveQuote: function(renderQuote) {
 -              this._proxy.setOption('data', {
 -                      actionName: 'saveQuote',
 -                      className: this._className,
 -                      interfaceName: 'wcf\\data\\IMessageQuoteAction',
 -                      objectIDs: [ this._objectID ],
 -                      parameters: {
 -                              message: this._message,
 -                              renderQuote: (renderQuote === true)
 +                      
 +                      // check if mousedown/mouseup took place inside a blockquote
 +                      var range = selection.getRangeAt(0);
 +                      var startContainer = (range.startContainer.nodeType === Node.TEXT_NODE) ? range.startContainer.parentNode : range.startContainer;
 +                      var endContainer = (range.endContainer.nodeType === Node.TEXT_NODE) ? range.endContainer.parentNode : range.endContainer;
 +                      if (startContainer.closest('blockquote') || endContainer.closest('blockquote')) {
 +                              this._copyQuote.removeClass('active');
 +                              
 +                              return;
                        }
 -              });
 -              this._proxy.sendRequest();
 -      },
 -      
 -      /**
 -       * Saves a quote and directly inserts it.
 -       */
 -      _saveAndInsertQuote: function() {
 -              this._saveQuote(true);
 -      },
 -      
 -      /**
 -       * Handles successful AJAX requests.
 -       * 
 -       * @param       {Object}        data
 -       */
 -      _success: function(data) {
 -              if (data.returnValues.count !== undefined) {
 -                      if (data.returnValues.fullQuoteMessageIDs !== undefined) {
 -                              data.returnValues.fullQuoteObjectIDs = data.returnValues.fullQuoteMessageIDs;
 +                      
 +                      // compare selection with message text of given container
 +                      var $messageText = this._getNodeText($container[0]);
 +                      
 +                      // selected text is not part of $messageText or contains text from unrelated nodes
 +                      if (this._normalize($messageText).indexOf(this._normalize($text)) === -1) {
 +                              return;
                        }
 +                      this._copyQuote.addClass('active');
                        
 -                      var $fullQuoteObjectIDs = (data.returnValues.fullQuoteObjectIDs !== undefined) ? data.returnValues.fullQuoteObjectIDs : { };
 -                      this._quoteManager.updateCount(data.returnValues.count, $fullQuoteObjectIDs);
 -              }
 -              
 -              switch (data.actionName) {
 -                      case 'saveQuote':
 -                      case 'saveFullQuote':
 -                              if (data.returnValues.renderedQuote) {
 -                                      WCF.System.Event.fireEvent('com.woltlab.wcf.message.quote', 'insert', {
 -                                              forceInsert: (data.actionName === 'saveQuote'),
 -                                              quote: data.returnValues.renderedQuote
 -                                      });
 +                      var $coordinates = this._getBoundingRectangle($container, window.getSelection());
 +                      var $dimensions = this._copyQuote.getDimensions('outer');
 +                      var $left = ($coordinates.right - $coordinates.left) / 2 - ($dimensions.width / 2) + $coordinates.left;
 +                      
 +                      this._copyQuote.css({
 +                              top: $coordinates.top - $dimensions.height - 7 + 'px',
 +                              left: $left + 'px'
 +                      });
 +                      this._copyQuote.removeClass('active');
 +                      
 +                      // reset containerID
 +                      this._activeContainerID = '';
 +                      
 +                      // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
 +                      var self = this;
 +                      window.setTimeout(function () {
 +                              var $text = $.trim(self._getSelectedText());
 +                              if ($text != '') {
 +                                      self._copyQuote.addClass('active');
 +                                      self._message = $text;
 +                                      self._objectID = $objectID;
                                }
 -                      break;
 -              }
 -      },
 -      
 -      /**
 -       * Updates the full quote data for all matching objects.
 -       * 
 -       * @param       array<integer>          $objectIDs
 -       */
 -      updateFullQuoteObjectIDs: function(objectIDs) {
 -              for (var $containerID in this._containers) {
 -                      this._containers[$containerID].find('.jsQuoteMessage').each(function(index, button) {
 -                              // reset all markings
 -                              var $button = $(button).data('isQuoted', 0);
 -                              $button.children('a').removeClass('active');
 +                      }, 10);
 +              },
 +              
 +              /**
 +               * Normalizes a text for comparison.
 +               *
 +               * @param        {string}        text
 +               * @return        {string}
 +               */
 +              _normalize: function (text) {
 +                      return text.replace(/\r?\n|\r/g, "\n").replace(/\s/g, ' ').replace(/\s{2,}/g, ' ');
 +              },
 +              
 +              /**
 +               * Returns the offsets of the selection's bounding rectangle.
 +               *
 +               * @return        {Object}
 +               */
 +              _getBoundingRectangle: function (container, selection) {
 +                      var $coordinates = null;
 +                      
 +                      if (selection.rangeCount > 0) {
 +                              // the coordinates returned by getBoundingClientRect() are relative to the viewport, not the document!
 +                              var $rect = selection.getRangeAt(0).getBoundingClientRect();
                                
 -                              // mark as active
 -                              if (WCF.inArray($button.data('objectID'), objectIDs)) {
 -                                      $button.data('isQuoted', 1).children('a').addClass('active');
 +                              $coordinates = {
 +                                      left: $rect.left,
 +                                      right: $rect.right,
 +                                      top: $rect.top + $(document).scrollTop()
 +                              };
 +                      }
 +                      
 +                      return $coordinates;
 +              },
 +              
 +              /**
 +               * Initializes the 'copy quote' element.
 +               *
 +               * @param        {boolean}        supportDirectInsert
 +               */
 +              _initCopyQuote: function (supportDirectInsert) {
 +                      this._copyQuote = $('#quoteManagerCopy');
 +                      if (!this._copyQuote.length) {
 +                              this._copyQuote = $('<div id="quoteManagerCopy" class="balloonTooltip interactive"><span class="jsQuoteManagerStore">' + WCF.Language.get('wcf.message.quote.quoteSelected') + '</span></div>').appendTo(document.body);
 +                              var $storeQuote = this._copyQuote.children('span.jsQuoteManagerStore').click($.proxy(this._saveQuote, this));
 +                              if (supportDirectInsert) {
 +                                      $('<span class="jsQuoteManagerQuoteAndInsert">' + WCF.Language.get('wcf.message.quote.quoteAndReply') + '</span>').click($.proxy(this._saveAndInsertQuote, this)).insertAfter($storeQuote);
                                }
 +                      }
 +              },
 +              
 +              /**
 +               * Returns the text selection.
 +               *
 +               * @return        string
 +               */
 +              _getSelectedText: function () {
 +                      var $selection = window.getSelection();
 +                      if ($selection.rangeCount) {
 +                              return this._getNodeText($selection.getRangeAt(0).cloneContents());
 +                      }
 +                      
 +                      return '';
 +              },
 +              
 +              /**
 +               * Saves a full quote.
 +               *
 +               * @param        {Event}                event
 +               */
 +              _saveFullQuote: function (event) {
 +                      event.preventDefault();
 +                      
 +                      var $listItem = $(event.currentTarget);
 +                      
 +                      this._proxy.setOption('data', {
 +                              actionName: 'saveFullQuote',
 +                              className: this._className,
 +                              interfaceName: 'wcf\\data\\IMessageQuoteAction',
 +                              objectIDs: [$listItem.data('objectID')]
                        });
 -              }
 -      }
 -});
 -
 -/**
 - * Manages stored quotes.
 - * 
 - * @param     integer         count
 - */
 -WCF.Message.Quote.Manager = Class.extend({
 -      /**
 -       * list of form buttons
 -       * @var {Object}
 -       */
 -      _buttons: {},
 -      
 -      /**
 -       * number of stored quotes
 -       * @var {int}
 -       */
 -      _count: 0,
 -      
 -      /**
 -       * dialog overlay
 -       * @var {jQuery}
 -       */
 -      _dialog: null,
 -      
 -      /**
 -       * editor element id
 -       * @var {string}
 -       */
 -      _editorId: '',
 -      
 -      /**
 -       * alternative editor element id
 -       * @var {string}
 -       */
 -      _editorIdAlternative: '',
 -      
 -      /**
 -       * form element
 -       * @var {jQuery}
 -       */
 -      _form: null,
 -      
 -      /**
 -       * list of quote handlers
 -       * @var {Object}
 -       */
 -      _handlers: {},
 -      
 -      /**
 -       * true, if an up-to-date template exists
 -       * @var {boolean}
 -       */
 -      _hasTemplate: false,
 -      
 -      /**
 -       * true, if related quotes should be inserted
 -       * @var {boolean}
 -       */
 -      _insertQuotes: true,
 -      
 -      /**
 -       * action proxy
 -       * @var {WCF.Action.Proxy}
 -       */
 -      _proxy: null,
 -      
 -      /**
 -       * list of quotes to remove upon submit
 -       * @var {Array}
 -       */
 -      _removeOnSubmit: [ ],
 -      
 -      /**
 -       * allow pasting
 -       * @var {boolean}
 -       */
 -      _supportPaste: false,
 -      
 -      /**
 -       * pasting was temporarily enabled due to an alternative editor being set
 -       * @var boolean
 -       */
 -      _supportPasteOverride: false,
 -      
 -      /**
 -       * Initializes the quote manager.
 -       * 
 -       * @param       {int}           count
 -       * @param       {string}        elementID
 -       * @param       {boolean}       supportPaste
 -       * @param       {Array}         removeOnSubmit
 -       */
 -      init: function(count, elementID, supportPaste, removeOnSubmit) {
 -              this._buttons = {
 -                      insert: null,
 -                      remove: null
 -              };
 -              this._count = parseInt(count) || 0;
 -              this._dialog = null;
 -              this._editorId = '';
 -              this._editorIdAlternative = '';
 -              this._form = null;
 -              this._handlers = { };
 -              this._hasTemplate = false;
 -              this._insertQuotes = true;
 -              this._removeOnSubmit = [];
 -              this._supportPaste = false;
 -              this._supportPasteOverride = false;
 -              
 -              if (elementID) {
 -                      var element = $('#' + elementID);
 -                      if (element.length) {
 -                              this._editorId = elementID;
 -                              this._supportPaste = true;
 -                              
 -                              // get surrounding form-tag
 -                              this._form = element.parents('form:eq(0)');
 -                              if (this._form.length) {
 -                                      this._form.submit(this._submit.bind(this));
 -                                      this._removeOnSubmit = removeOnSubmit || [];
 +                      this._proxy.sendRequest();
 +                      
 +                      // mark element as quoted
 +                      if ($listItem.data('isQuoted')) {
 +                              $listItem.data('isQuoted', false).children('a').removeClass('active');
 +                      }
 +                      else {
 +                              $listItem.data('isQuoted', true).children('a').addClass('active');
 +                      }
 +                      
 +                      // close navigation on mobile
 +                      var $navigationList = $listItem.parents('.buttonGroupNavigation');
 +                      if ($navigationList.hasClass('jsMobileButtonGroupNavigation')) {
 +                              $navigationList.children('.dropdownLabel').trigger('click');
 +                      }
 +              },
 +              
 +              /**
 +               * Saves a quote.
 +               *
 +               * @param        {boolean}        renderQuote
 +               */
 +              _saveQuote: function (renderQuote) {
 +                      this._proxy.setOption('data', {
 +                              actionName: 'saveQuote',
 +                              className: this._className,
 +                              interfaceName: 'wcf\\data\\IMessageQuoteAction',
 +                              objectIDs: [this._objectID],
 +                              parameters: {
 +                                      message: this._message,
 +                                      renderQuote: (renderQuote === true)
                                }
 -                              else {
 -                                      this._form = null;
 -                                      
 -                                      // allow override
 -                                      this._supportPaste = (supportPaste === true);
 +                      });
 +                      this._proxy.sendRequest();
 +              },
 +              
 +              /**
 +               * Saves a quote and directly inserts it.
 +               */
 +              _saveAndInsertQuote: function () {
 +                      this._saveQuote(true);
 +              },
 +              
 +              /**
 +               * Handles successful AJAX requests.
 +               *
 +               * @param        {Object}        data
 +               */
 +              _success: function (data) {
 +                      if (data.returnValues.count !== undefined) {
 +                              if (data.returnValues.fullQuoteMessageIDs !== undefined) {
 +                                      data.returnValues.fullQuoteObjectIDs = data.returnValues.fullQuoteMessageIDs;
                                }
 +                              
 +                              var $fullQuoteObjectIDs = (data.returnValues.fullQuoteObjectIDs !== undefined) ? data.returnValues.fullQuoteObjectIDs : {};
 +                              this._quoteManager.updateCount(data.returnValues.count, $fullQuoteObjectIDs);
 +                      }
 +                      
 +                      switch (data.actionName) {
 +                              case 'saveQuote':
 +                              case 'saveFullQuote':
 +                                      if (data.returnValues.renderedQuote) {
 +                                              WCF.System.Event.fireEvent('com.woltlab.wcf.message.quote', 'insert', {
 +                                                      forceInsert: (data.actionName === 'saveQuote'),
 +                                                      quote: data.returnValues.renderedQuote
 +                                              });
 +                                      }
 +                                      break;
 +                      }
 +              },
 +              
 +              /**
 +               * Updates the full quote data for all matching objects.
 +               *
 +               * @param        array<integer>                $objectIDs
 +               */
 +              updateFullQuoteObjectIDs: function (objectIDs) {
 +                      for (var $containerID in this._containers) {
 +                              this._containers[$containerID].find('.jsQuoteMessage').each(function (index, button) {
 +                                      // reset all markings
 +                                      var $button = $(button).data('isQuoted', 0);
 +                                      $button.children('a').removeClass('active');
 +                                      
 +                                      // mark as active
 +                                      if (WCF.inArray($button.data('objectID'), objectIDs)) {
 +                                              $button.data('isQuoted', 1).children('a').addClass('active');
 +                                      }
 +                              });
                        }
                }
 -              
 -              this._proxy = new WCF.Action.Proxy({
 -                      showLoadingOverlay: false,
 -                      success: $.proxy(this._success, this),
 -                      url: 'index.php?message-quote/&t=' + SECURITY_TOKEN
 -              });
 -              
 -              this._toggleShowQuotes();
 -              
 -              WCF.System.Event.addListener('com.woltlab.wcf.quote', 'reload', this.countQuotes.bind(this));
 -              
 -              // event forwarding
 -              WCF.System.Event.addListener('com.woltlab.wcf.message.quote', 'insert', (function(data) {
 -                      //noinspection JSUnresolvedVariable
 -                      WCF.System.Event.fireEvent('com.woltlab.wcf.redactor2', 'insertQuote_' + (this._editorIdAlternative ? this._editorIdAlternative : this._editorId), {
 -                              author: data.quote.username,
 -                              content: data.quote.text,
 -                              isText: !data.quote.isFullQuote,
 -                              link: data.quote.link
 -                      });
 -              }).bind(this));
 -      },
 -      
 -      /**
 -       * Sets an alternative editor element id on runtime.
 -       * 
 -       * @param       {(string|jQuery)}       elementId       element id or jQuery element
 -       */
 -      setAlternativeEditor: function(elementId) {
 -              if (!this._editorIdAlternative && !this._supportPaste) {
 -                      this._hasTemplate = false;
 -                      this._supportPaste = true;
 -                      this._supportPasteOverride = true;
 -              }
 -              
 -              if (typeof elementId === 'object') elementId = elementId[0].id;
 -              this._editorIdAlternative = elementId;
 -      },
 -      
 -      /**
 -       * Clears alternative editor element id.
 -       */
 -      clearAlternativeEditor: function() {
 -              if (this._supportPasteOverride) {
 +      });
 +      
 +      /**
 +       * Manages stored quotes.
 +       *
 +       * @param        integer                count
 +       */
 +      WCF.Message.Quote.Manager = Class.extend({
 +              /**
 +               * list of form buttons
 +               * @var        {Object}
 +               */
 +              _buttons: {},
 +              
 +              /**
 +               * number of stored quotes
 +               * @var        {int}
 +               */
 +              _count: 0,
 +              
 +              /**
 +               * dialog overlay
 +               * @var        {jQuery}
 +               */
 +              _dialog: null,
 +              
 +              /**
 +               * editor element id
 +               * @var        {string}
 +               */
 +              _editorId: '',
 +              
 +              /**
 +               * alternative editor element id
 +               * @var        {string}
 +               */
 +              _editorIdAlternative: '',
 +              
 +              /**
 +               * form element
 +               * @var        {jQuery}
 +               */
 +              _form: null,
 +              
 +              /**
 +               * list of quote handlers
 +               * @var        {Object}
 +               */
 +              _handlers: {},
 +              
 +              /**
 +               * true, if an up-to-date template exists
 +               * @var        {boolean}
 +               */
 +              _hasTemplate: false,
 +              
 +              /**
 +               * true, if related quotes should be inserted
 +               * @var        {boolean}
 +               */
 +              _insertQuotes: true,
 +              
 +              /**
 +               * action proxy
 +               * @var        {WCF.Action.Proxy}
 +               */
 +              _proxy: null,
 +              
 +              /**
 +               * list of quotes to remove upon submit
 +               * @var        {Array}
 +               */
 +              _removeOnSubmit: [],
 +              
 +              /**
 +               * allow pasting
 +               * @var        {boolean}
 +               */
 +              _supportPaste: false,
 +              
++              /**
++               * pasting was temporarily enabled due to an alternative editor being set
++               * @var boolean
++               */
++              _supportPasteOverride: false,
++              
 +              /**
 +               * Initializes the quote manager.
 +               *
 +               * @param        {int}                count
 +               * @param        {string}        elementID
 +               * @param        {boolean}        supportPaste
 +               * @param        {Array}        removeOnSubmit
 +               */
 +              init: function (count, elementID, supportPaste, removeOnSubmit) {
 +                      this._buttons = {
 +                              insert: null,
 +                              remove: null
 +                      };
 +                      this._count = parseInt(count) || 0;
 +                      this._dialog = null;
 +                      this._editorId = '';
 +                      this._editorIdAlternative = '';
 +                      this._form = null;
 +                      this._handlers = {};
                        this._hasTemplate = false;
 +                      this._insertQuotes = true;
 +                      this._removeOnSubmit = [];
                        this._supportPaste = false;
 -              }
 -              
 -              this._editorIdAlternative = '';
 -      },
 -      
 -      /**
 -       * Registers a quote handler.
 -       * 
 -       * @param       {string}                        objectType
 -       * @param       {WCF.Message.Quote.Handler}     handler
 -       */
 -      register: function(objectType, handler) {
 -              this._handlers[objectType] = handler;
 -      },
 -      
 -      /**
 -       * Updates number of stored quotes.
 -       * 
 -       * @param       {int}           count
 -       * @param       {Object}        fullQuoteObjectIDs
 -       */
 -      updateCount: function(count, fullQuoteObjectIDs) {
 -              this._count = parseInt(count) || 0;
 -              
 -              this._toggleShowQuotes();
 -              
 -              // update full quote ids of handlers
 -              for (var $objectType in this._handlers) {
 -                      if (this._handlers.hasOwnProperty($objectType)) {
 -                              var $objectIDs = fullQuoteObjectIDs[$objectType] || [];
 -                              this._handlers[$objectType].updateFullQuoteObjectIDs($objectIDs);
+                       this._supportPasteOverride = false;
 +                      
 +                      if (elementID) {
 +                              var element = $('#' + elementID);
 +                              if (element.length) {
 +                                      this._editorId = elementID;
 +                                      this._supportPaste = true;
 +                                      
 +                                      // get surrounding form-tag
 +                                      this._form = element.parents('form:eq(0)');
 +                                      if (this._form.length) {
 +                                              this._form.submit(this._submit.bind(this));
 +                                              this._removeOnSubmit = removeOnSubmit || [];
 +                                      }
 +                                      else {
 +                                              this._form = null;
 +                                              
 +                                              // allow override
 +                                              this._supportPaste = (supportPaste === true);
 +                                      }
 +                              }
                        }
 -              }
 -      },
 -      
 -      /**
 -       * Inserts all associated quotes upon first time using quick reply.
 -       * 
 -       * @param       {string}        className
 -       * @param       {int}           parentObjectID
 -       * @param       {Object}        callback
 -       */
 -      insertQuotes: function(className, parentObjectID, callback) {
 -              if (!this._insertQuotes) {
 -                      this._insertQuotes = true;
                        
 -                      return;
 -              }
 -              
 -              new WCF.Action.Proxy({
 -                      autoSend: true,
 -                      data: {
 -                              actionName: 'getRenderedQuotes',
 -                              className: className,
 -                              interfaceName: 'wcf\\data\\IMessageQuoteAction',
 -                              parameters: {
 -                                      parentObjectID: parentObjectID
 +                      this._proxy = new WCF.Action.Proxy({
 +                              showLoadingOverlay: false,
 +                              success: $.proxy(this._success, this),
 +                              url: 'index.php?message-quote/&t=' + SECURITY_TOKEN
 +                      });
 +                      
 +                      this._toggleShowQuotes();
 +                      
 +                      WCF.System.Event.addListener('com.woltlab.wcf.quote', 'reload', this.countQuotes.bind(this));
 +                      
 +                      // event forwarding
 +                      WCF.System.Event.addListener('com.woltlab.wcf.message.quote', 'insert', (function (data) {
 +                              //noinspection JSUnresolvedVariable
 +                              WCF.System.Event.fireEvent('com.woltlab.wcf.redactor2', 'insertQuote_' + (this._editorIdAlternative ? this._editorIdAlternative : this._editorId), {
 +                                      author: data.quote.username,
 +                                      content: data.quote.text,
 +                                      isText: !data.quote.isFullQuote,
 +                                      link: data.quote.link
 +                              });
 +                      }).bind(this));
 +              },
 +              
 +              /**
 +               * Sets an alternative editor element id on runtime.
 +               *
 +               * @param        {(string|jQuery)}       elementId       element id or jQuery element
 +               */
 +              setAlternativeEditor: function (elementId) {
++                      if (!this._editorIdAlternative && !this._supportPaste) {
++                              this._hasTemplate = false;
++                              this._supportPaste = true;
++                              this._supportPasteOverride = true;
++                      }
++                      
 +                      if (typeof elementId === 'object') elementId = elementId[0].id;
 +                      this._editorIdAlternative = elementId;
 +              },
 +              
 +              /**
 +               * Clears alternative editor element id.
 +               */
 +              clearAlternativeEditor: function () {
++                      if (this._supportPasteOverride) {
++                              this._hasTemplate = false;
++                              this._supportPaste = false;
++                              this._supportPasteOverride = false;
++                      }
++                      
 +                      this._editorIdAlternative = '';
 +              },
 +              
 +              /**
 +               * Registers a quote handler.
 +               *
 +               * @param        {string}                        objectType
 +               * @param        {WCF.Message.Quote.Handler}        handler
 +               */
 +              register: function (objectType, handler) {
 +                      this._handlers[objectType] = handler;
 +              },
 +              
 +              /**
 +               * Updates number of stored quotes.
 +               *
 +               * @param        {int}                count
 +               * @param        {Object}        fullQuoteObjectIDs
 +               */
 +              updateCount: function (count, fullQuoteObjectIDs) {
 +                      this._count = parseInt(count) || 0;
 +                      
 +                      this._toggleShowQuotes();
 +                      
 +                      // update full quote ids of handlers
 +                      for (var $objectType in this._handlers) {
 +                              if (this._handlers.hasOwnProperty($objectType)) {
 +                                      var $objectIDs = fullQuoteObjectIDs[$objectType] || [];
 +                                      this._handlers[$objectType].updateFullQuoteObjectIDs($objectIDs);
                                }
 -                      },
 -                      success: callback
 -              });
 -      },
 -      
 -      /**
 -       * Toggles the display of the 'Show quotes' button
 -       */
 -      _toggleShowQuotes: function() {
 -              require(['WoltLabSuite/Core/Ui/Page/Action'], (function(UiPageAction) {
 -                      var buttonName = 'showQuotes';
 -                      
 -                      if (this._count) {
 -                              var button = UiPageAction.get(buttonName);
 -                              if (button === undefined) {
 -                                      button = elCreate('a');
 -                                      button.addEventListener('mousedown', this._click.bind(this));
 +                      }
 +              },
 +              
 +              /**
 +               * Inserts all associated quotes upon first time using quick reply.
 +               *
 +               * @param        {string}        className
 +               * @param        {int}                parentObjectID
 +               * @param        {Object}        callback
 +               */
 +              insertQuotes: function (className, parentObjectID, callback) {
 +                      if (!this._insertQuotes) {
 +                              this._insertQuotes = true;
 +                              
 +                              return;
 +                      }
 +                      
 +                      new WCF.Action.Proxy({
 +                              autoSend: true,
 +                              data: {
 +                                      actionName: 'getRenderedQuotes',
 +                                      className: className,
 +                                      interfaceName: 'wcf\\data\\IMessageQuoteAction',
 +                                      parameters: {
 +                                              parentObjectID: parentObjectID
 +                                      }
 +                              },
 +                              success: callback
 +                      });
 +              },
 +              
 +              /**
 +               * Toggles the display of the 'Show quotes' button
 +               */
 +              _toggleShowQuotes: function () {
 +                      require(['WoltLabSuite/Core/Ui/Page/Action'], (function (UiPageAction) {
 +                              var buttonName = 'showQuotes';
 +                              
 +                              if (this._count) {
 +                                      var button = UiPageAction.get(buttonName);
 +                                      if (button === undefined) {
 +                                              button = elCreate('a');
 +                                              button.addEventListener('mousedown', this._click.bind(this));
 +                                              
 +                                              UiPageAction.add(buttonName, button);
 +                                      }
                                        
 -                                      UiPageAction.add(buttonName, button);
 +                                      button.textContent = WCF.Language.get('wcf.message.quote.showQuotes').replace(/#count#/, this._count);
 +                                      
 +                                      UiPageAction.show(buttonName);
 +                              }
 +                              else {
 +                                      UiPageAction.hide(buttonName);
                                }
                                
 -                              button.textContent = WCF.Language.get('wcf.message.quote.showQuotes').replace(/#count#/, this._count);
 +                              this._hasTemplate = false;
 +                      }).bind(this));
 +              },
 +              
 +              /**
 +               * Handles clicks on 'Show quotes'.
 +               */
 +              _click: function () {
 +                      var editor = document.activeElement;
 +                      if (editor.classList.contains('redactor-layer')) {
 +                              $('#' + elData(editor, 'element-id')).redactor('selection.save');
 +                      }
 +                      
 +                      if (this._hasTemplate) {
 +                              this._dialog.wcfDialog('open');
 +                      }
 +                      else {
 +                              this._proxy.showLoadingOverlayOnce();
                                
 -                              UiPageAction.show(buttonName);
 +                              this._proxy.setOption('data', {
 +                                      actionName: 'getQuotes',
 +                                      supportPaste: this._supportPaste
 +                              });
 +                              this._proxy.sendRequest();
 +                      }
 +              },
 +              
 +              /**
 +               * Renders the dialog.
 +               *
 +               * @param        {string}        template
 +               */
 +              renderDialog: function (template) {
 +                      // create dialog if not exists
 +                      if (this._dialog === null) {
 +                              this._dialog = $('#messageQuoteList');
 +                              if (!this._dialog.length) {
 +                                      this._dialog = $('<div id="messageQuoteList" />').hide().appendTo(document.body);
 +                              }
 +                      }
 +                      
 +                      // add template
 +                      this._dialog.html(template);
 +                      
 +                      // add 'insert' and 'delete' buttons
 +                      var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog);
 +                      if (this._supportPaste) this._buttons.insert = $('<button class="buttonPrimary">' + WCF.Language.get('wcf.message.quote.insertAllQuotes') + '</button>').click($.proxy(this._insertSelected, this)).appendTo($formSubmit);
 +                      this._buttons.remove = $('<button>' + WCF.Language.get('wcf.message.quote.removeAllQuotes') + '</button>').click($.proxy(this._removeSelected, this)).appendTo($formSubmit);
 +                      
 +                      // show dialog
 +                      this._dialog.wcfDialog({
 +                              title: WCF.Language.get('wcf.message.quote.manageQuotes')
 +                      });
 +                      this._dialog.wcfDialog('render');
 +                      this._hasTemplate = true;
 +                      
 +                      // bind event listener
 +                      var $insertQuoteButtons = this._dialog.find('.jsInsertQuote');
 +                      if (this._supportPaste) {
 +                              $insertQuoteButtons.click($.proxy(this._insertQuote, this));
                        }
                        else {
 -                              UiPageAction.hide(buttonName);
 +                              $insertQuoteButtons.hide();
                        }
                        
 -                      this._hasTemplate = false;
 -              }).bind(this));
 -      },
 -      
 -      /**
 -       * Handles clicks on 'Show quotes'.
 -       */
 -      _click: function() {
 -              var editor = document.activeElement;
 -              if (editor.classList.contains('redactor-layer')) {
 -                      $('#' + elData(editor, 'element-id')).redactor('selection.save');
 -              }
 -              
 -              if (this._hasTemplate) {
 -                      this._dialog.wcfDialog('open');
 -              }
 -              else {
 -                      this._proxy.showLoadingOverlayOnce();
 +                      this._dialog.find('input.jsCheckbox').change($.proxy(this._changeButtons, this));
                        
 -                      this._proxy.setOption('data', {
 -                              actionName: 'getQuotes',
 -                              supportPaste: this._supportPaste
 +                      // mark quotes for removal
 +                      if (this._removeOnSubmit.length) {
 +                              var self = this;
 +                              this._dialog.find('input.jsRemoveQuote').each(function (index, input) {
 +                                      var $input = $(input).change($.proxy(this._change, this));
 +                                      
 +                                      // mark for deletion
 +                                      if (WCF.inArray($input.parent('li').attr('data-quote-id'), self._removeOnSubmit)) {
 +                                              $input.attr('checked', 'checked');
 +                                      }
 +                              });
 +                      }
 +              },
 +              
 +              /**
 +               * Updates button labels if a checkbox is checked or unchecked.
 +               */
 +              _changeButtons: function () {
 +                      // selection
 +                      if (this._dialog.find('input.jsCheckbox:checked').length) {
 +                              if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertSelectedQuotes'));
 +                              this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeSelectedQuotes'));
 +                      }
 +                      else {
 +                              // no selection, pick all
 +                              if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertAllQuotes'));
 +                              this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeAllQuotes'));
 +                      }
 +              },
 +              
 +              /**
 +               * Checks for change event on delete-checkboxes.
 +               *
 +               * @param        {Object}        event
 +               */
 +              _change: function (event) {
 +                      var $input = $(event.currentTarget);
 +                      var $quoteID = $input.parent('li').attr('data-quote-id');
 +                      
 +                      if ($input.prop('checked')) {
 +                              this._removeOnSubmit.push($quoteID);
 +                      }
 +                      else {
 +                              var index = this._removeOnSubmit.indexOf($quoteID);
 +                              if (index !== -1) {
 +                                      this._removeOnSubmit.splice(index, 1);
 +                              }
 +                      }
 +              },
 +              
 +              /**
 +               * Inserts the selected quotes.
 +               */
 +              _insertSelected: function () {
 +                      if (!this._dialog.find('input.jsCheckbox:checked').length) {
 +                              this._dialog.find('input.jsCheckbox').prop('checked', 'checked');
 +                      }
 +                      
 +                      // insert all quotes
 +                      this._dialog.find('input.jsCheckbox:checked').each($.proxy(function (index, input) {
 +                              this._insertQuote(null, input);
 +                      }, this));
 +                      
 +                      // close dialog
 +                      this._dialog.wcfDialog('close');
 +              },
 +              
 +              /**
 +               * Inserts a quote.
 +               *
 +               * @param        {Event}                event
 +               * @param        {Object}        inputElement
 +               */
 +              _insertQuote: function (event, inputElement) {
 +                      var listItem = $(event ? event.currentTarget : inputElement).parents('li:eq(0)');
 +                      var text = listItem.children('.jsFullQuote')[0].textContent.trim();
 +                      
 +                      var message = listItem.parents('.message:eq(0)');
 +                      var author = message.data('username');
 +                      var link = message.data('link');
 +                      var isText = !elDataBool(listItem[0], 'is-full-quote');
 +                      
 +                      WCF.System.Event.fireEvent('com.woltlab.wcf.redactor2', 'insertQuote_' + (this._editorIdAlternative ? this._editorIdAlternative : this._editorId), {
 +                              author: author,
 +                              content: text,
 +                              isText: isText,
 +                              link: link
                        });
 -                      this._proxy.sendRequest();
 -              }
 -      },
 -      
 -      /**
 -       * Renders the dialog.
 -       * 
 -       * @param       {string}        template
 -       */
 -      renderDialog: function(template) {
 -              // create dialog if not exists
 -              if (this._dialog === null) {
 -                      this._dialog = $('#messageQuoteList');
 -                      if (!this._dialog.length) {
 -                              this._dialog = $('<div id="messageQuoteList" />').hide().appendTo(document.body);
 +                      
 +                      // remove quote upon submit or upon request
 +                      this._removeOnSubmit.push(listItem.data('quote-id'));
 +                      
 +                      // close dialog
 +                      if (event !== null) {
 +                              this._dialog.wcfDialog('close');
                        }
 -              }
 -              
 -              // add template
 -              this._dialog.html(template);
 -              
 -              // add 'insert' and 'delete' buttons
 -              var $formSubmit = $('<div class="formSubmit" />').appendTo(this._dialog);
 -              if (this._supportPaste) this._buttons.insert = $('<button class="buttonPrimary">' + WCF.Language.get('wcf.message.quote.insertAllQuotes') + '</button>').click($.proxy(this._insertSelected, this)).appendTo($formSubmit);
 -              this._buttons.remove = $('<button>' + WCF.Language.get('wcf.message.quote.removeAllQuotes') + '</button>').click($.proxy(this._removeSelected, this)).appendTo($formSubmit);
 -              
 -              // show dialog
 -              this._dialog.wcfDialog({
 -                      title: WCF.Language.get('wcf.message.quote.manageQuotes')
 -              });
 -              this._dialog.wcfDialog('render');
 -              this._hasTemplate = true;
 -              
 -              // bind event listener
 -              var $insertQuoteButtons = this._dialog.find('.jsInsertQuote');
 -              if (this._supportPaste) {
 -                      $insertQuoteButtons.click($.proxy(this._insertQuote, this));
 -              }
 -              else {
 -                      $insertQuoteButtons.hide();
 -              }
 -              
 -              this._dialog.find('input.jsCheckbox').change($.proxy(this._changeButtons, this));
 -              
 -              // mark quotes for removal
 -              if (this._removeOnSubmit.length) {
 -                      var self = this;
 -                      this._dialog.find('input.jsRemoveQuote').each(function(index, input) {
 -                              var $input = $(input).change($.proxy(this._change, this));
 +              },
 +              
 +              /**
 +               * Removes selected quotes.
 +               */
 +              _removeSelected: function () {
 +                      if (!this._dialog.find('input.jsCheckbox:checked').length) {
 +                              this._dialog.find('input.jsCheckbox').prop('checked', 'checked');
 +                      }
 +                      
 +                      var $quoteIDs = [];
 +                      this._dialog.find('input.jsCheckbox:checked').each(function (index, input) {
 +                              $quoteIDs.push($(input).parents('li').attr('data-quote-id'));
 +                      });
 +                      
 +                      if ($quoteIDs.length) {
 +                              // get object types
 +                              var $objectTypes = [];
 +                              for (var $objectType in this._handlers) {
 +                                      if (this._handlers.hasOwnProperty($objectType)) {
 +                                              $objectTypes.push($objectType);
 +                                      }
 +                              }
 +                              
 +                              this._proxy.setOption('data', {
 +                                      actionName: 'remove',
 +                                      getFullQuoteObjectIDs: this._handlers.length > 0,
 +                                      objectTypes: $objectTypes,
 +                                      quoteIDs: $quoteIDs
 +                              });
 +                              this._proxy.sendRequest();
                                
 -                              // mark for deletion
 -                              if (WCF.inArray($input.parent('li').attr('data-quote-id'), self._removeOnSubmit)) {
 -                                      $input.attr('checked', 'checked');
 +                              this._dialog.wcfDialog('close');
 +                      }
 +              },
 +              
 +              /**
 +               * Appends list of quote ids to remove after successful submit.
 +               */
 +              _submit: function () {
 +                      if (this._supportPaste && this._removeOnSubmit.length > 0) {
 +                              var $formSubmit = this._form.find('.formSubmit');
 +                              for (var i = 0, length = this._removeOnSubmit.length; i < length; i++) {
 +                                      $('<input type="hidden" name="__removeQuoteIDs[]" value="' + this._removeOnSubmit[i] + '" />').appendTo($formSubmit);
                                }
 -                      });
 -              }
 -      },
 -      
 -      /**
 -       * Updates button labels if a checkbox is checked or unchecked.
 -       */
 -      _changeButtons: function() {
 -              // selection
 -              if (this._dialog.find('input.jsCheckbox:checked').length) {
 -                      if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertSelectedQuotes'));
 -                      this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeSelectedQuotes'));
 -              }
 -              else {
 -                      // no selection, pick all
 -                      if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertAllQuotes'));
 -                      this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeAllQuotes'));
 -              }
 -      },
 -      
 -      /**
 -       * Checks for change event on delete-checkboxes.
 -       * 
 -       * @param       {Object}        event
 -       */
 -      _change: function(event) {
 -              var $input = $(event.currentTarget);
 -              var $quoteID = $input.parent('li').attr('data-quote-id');
 -              
 -              if ($input.prop('checked')) {
 -                      this._removeOnSubmit.push($quoteID);
 -              }
 -              else {
 -                      var index = this._removeOnSubmit.indexOf($quoteID);
 -                      if (index !== -1) {
 -                              this._removeOnSubmit.splice(index, 1);
                        }
 -              }
 -      },
 -      
 -      /**
 -       * Inserts the selected quotes.
 -       */
 -      _insertSelected: function() {
 -              if (!this._dialog.find('input.jsCheckbox:checked').length) {
 -                      this._dialog.find('input.jsCheckbox').prop('checked', 'checked');
 -              }
 -              
 -              // insert all quotes
 -              this._dialog.find('input.jsCheckbox:checked').each($.proxy(function(index, input) {
 -                      this._insertQuote(null, input);
 -              }, this));
 -              
 -              // close dialog
 -              this._dialog.wcfDialog('close');
 -      },
 -      
 -      /**
 -       * Inserts a quote.
 -       * 
 -       * @param       {Event}         event
 -       * @param       {Object}        inputElement
 -       */
 -      _insertQuote: function(event, inputElement) {
 -              var listItem = $(event ? event.currentTarget : inputElement).parents('li:eq(0)');
 -              var text = listItem.children('.jsFullQuote')[0].textContent.trim();
 -              
 -              var message = listItem.parents('.message:eq(0)');
 -              var author = message.data('username');
 -              var link = message.data('link');
 -              var isText = !elDataBool(listItem[0], 'is-full-quote');
 -              
 -              WCF.System.Event.fireEvent('com.woltlab.wcf.redactor2', 'insertQuote_' + (this._editorIdAlternative ? this._editorIdAlternative : this._editorId), {
 -                      author: author,
 -                      content: text,
 -                      isText: isText,
 -                      link: link
 -              });
 -              
 -              // remove quote upon submit or upon request
 -              this._removeOnSubmit.push(listItem.data('quote-id'));
 -              
 -              // close dialog
 -              if (event !== null) {
 -                      this._dialog.wcfDialog('close');
 -              }
 -      },
 -      
 -      /**
 -       * Removes selected quotes.
 -       */
 -      _removeSelected: function() {
 -              if (!this._dialog.find('input.jsCheckbox:checked').length) {
 -                      this._dialog.find('input.jsCheckbox').prop('checked', 'checked');
 -              }
 -              
 -              var $quoteIDs = [ ];
 -              this._dialog.find('input.jsCheckbox:checked').each(function(index, input) {
 -                      $quoteIDs.push($(input).parents('li').attr('data-quote-id'));
 -              });
 +              },
 +              
 +              /**
 +               * Returns a list of quote ids marked for removal.
 +               *
 +               * @return        {Array}
 +               */
 +              getQuotesMarkedForRemoval: function () {
 +                      return this._removeOnSubmit;
 +              },
 +              
 +              /**
 +               * Marks quote ids for removal.
 +               */
 +              markQuotesForRemoval: function () {
 +                      if (this._removeOnSubmit.length) {
 +                              this._proxy.setOption('data', {
 +                                      actionName: 'markForRemoval',
 +                                      quoteIDs: this._removeOnSubmit
 +                              });
 +                              this._proxy.suppressErrors();
 +                              this._proxy.sendRequest();
 +                      }
 +              },
 +              
 +              /**
 +               * Removes all marked quote ids.
 +               */
 +              removeMarkedQuotes: function () {
 +                      if (this._removeOnSubmit.length) {
 +                              this._proxy.setOption('data', {
 +                                      actionName: 'removeMarkedQuotes',
 +                                      getFullQuoteObjectIDs: this._handlers.length > 0
 +                              });
 +                              this._proxy.sendRequest();
 +                      }
 +              },
                
 -              if ($quoteIDs.length) {
 -                      // get object types
 +              /**
 +               * Counts stored quotes.
 +               */
 +              countQuotes: function () {
                        var $objectTypes = [];
                        for (var $objectType in this._handlers) {
                                if (this._handlers.hasOwnProperty($objectType)) {
index 4b75d0b35b9079caf55b98ecaf9080e8c92bc366,e879a38704f65b2e7c695ec9b22c6b4fa93153c6..5c862194eb0fdce0a4be4953235677dfb5d84dd3
                <item name="wcf.acp.index.woltlab.forums"><![CDATA[Supportforum]]></item>
                <item name="wcf.acp.index.woltlab.tickets"><![CDATA[Ticket-Support]]></item>
                <item name="wcf.acp.index.woltlab.pluginStore"><![CDATA[Plugin-Store]]></item>
-               <item name="wcf.acp.index.recaptchaWithoutKey"><![CDATA[Die Nutzung von ReCAPTCHA ohne einen individuellen Website-Schlüssel wird von Google nicht mehr unterstützt.]]></item>
 +              <item name="wcf.acp.index.tinyBuild"><![CDATA[Die Seitenbeschleunigung für Gäste verbessert die Ladezeiten für Besucher und Suchmaschinen, es wird empfohlen diese <a href="{link controller='Option' id=1 optionName="visitor_use_tiny_build"}#category_module.system{/link}">zu aktivieren</a>.]]></item>
+               <item name="wcf.acp.index.recaptchaWithoutKey"><![CDATA[Die Nutzung von reCAPTCHA ohne einen individuellen Website-Schlüssel wird von Google nicht mehr unterstützt.<br><br>Für eine weitere Nutzung {if LANGUAGE_USE_INFORMAL_VARIANT}musst du{else}müssen Sie{/if} <a href="{$recaptchaKeyLink}">einen Schlüssel in den Optionen hinterlegen</a>, unterhalb des Eingabefeldes befindet sich eine Anleitung zum Anfordern des Schlüssels.]]></item>
        </category>
        
        <category name="wcf.acp.label">
index 32c4dede747ed25e018c540a75f8c9e43a914ab9,74495d37330434187ed192797463e85437833526..1aa155b929bc4d1789afe93e1cbc2315ac1e81e3
                <item name="wcf.acp.index.woltlab.forums"><![CDATA[Support Forums]]></item>
                <item name="wcf.acp.index.woltlab.tickets"><![CDATA[Ticket Support]]></item>
                <item name="wcf.acp.index.woltlab.pluginStore"><![CDATA[Plugin Store]]></item>
-               <item name="wcf.acp.index.recaptchaWithoutKey"><![CDATA[Using ReCAPTCHA without an individual website key is no longer supported by Google.]]></item>
 +              <item name="wcf.acp.index.tinyBuild"><![CDATA[The accelerated guest view improves the page responsiveness and loading times for both visitors and search engines alike, please consider <a href="{link controller='Option' id=1 optionName="visitor_use_tiny_build"}#category_module.system{/link}">enabling it</a>.]]></item>
+               <item name="wcf.acp.index.recaptchaWithoutKey"><![CDATA[Using reCAPTCHA without an individual website key is no longer supported by Google.<br><br>For further use you need to <a href="{$recaptchaKeyLink}">provide a key in your options</a>, please follow the instructions below the input field to obtain a key.]]></item>
        </category>
        
        <category name="wcf.acp.label">