Replaced WCF.M;essage.QuickReply
authorAlexander Ebert <ebert@woltlab.com>
Sun, 31 Jan 2016 12:46:41 +0000 (13:46 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Sun, 31 Jan 2016 12:46:50 +0000 (13:46 +0100)
wcfsetup/install/files/js/WCF.Assets.js
wcfsetup/install/files/js/WCF.Message.js
wcfsetup/install/files/js/WoltLab/WCF/Dom/Util.js
wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js
wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Scroll.js [new file with mode: 0644]
wcfsetup/install/files/js/wcf.globalHelper.js
wcfsetup/install/files/style/ui/message.scss

index 145db8d8bf2f9f66f886e1e2a993541755b4e146..04f5e475447986ead4835dbb1585eed94aefa8cc 100644 (file)
@@ -53,5 +53,7 @@
  * Copyright 2013, Dustan Kasten
  * Released under the MIT license.
  * https://github.com/iamdustan/smoothscroll/blob/master/LICENSE
+ * 
+ * Version below includes the fix of PR 
  */
-!function(e,t,r){"use strict";function n(){return e.performance!==r&&e.performance.now!==r?e.performance.now():Date.now()}function o(e){return.5*(1-Math.cos(Math.PI*e))}function a(e){if("object"!=typeof e||e.behavior===r||"auto"===e.behavior||"instant"===e.behavior)return!0;if("smooth"===e.behavior)return!1;throw new TypeError(e.behavior+" is not a valid value for enumeration ScrollBehavior")}function l(e,t,r){e.scrollTop=r,e.scrollLeft=t}function i(e){return e.clientHeight<e.scrollHeight||e.clientWidth<e.scrollWidth?e:e.parentNode.parentNode?i(e.parentNode):void 0}function s(t,r){function a(){var u,p,g,v=n(),d=(v-s)/f;return d=d>1?1:d,u=o(d),p=l+(t-l)*u,g=i+(r-i)*u,m(p,g),p===t&&g===r?(l=i=s=null,void e.cancelAnimationFrame(c)):void(c=e.requestAnimationFrame(a))}var l=e.scrollX||e.pageXOffset,i=e.scrollY||e.pageYOffset,s=n();c&&e.cancelAnimationFrame(c),c=e.requestAnimationFrame(a)}function u(t,r){function a(){var r,g,v,d=n(),h=(d-p)/f;return h=h>1?1:h,r=o(h),g=i+(u-i)*r,v=s+(m-s)*r,l(t,g,v),g===u&&v===m?(i=s=p=null,void e.cancelAnimationFrame(c)):void(c=e.requestAnimationFrame(a))}var i=t.scrollLeft,s=t.scrollTop,u=r.left,m=r.top,p=n();c&&e.cancelAnimationFrame(c),c=e.requestAnimationFrame(a)}if(!("scrollBehavior"in t.documentElement.style)){var c,f=768,m=e.scrollTo,p=e.scrollBy,g=e.Element.prototype.scrollIntoView;e.scroll=e.scrollTo=function(){return a(arguments[0])?m.call(e,arguments[0].left||arguments[0],arguments[0].top||arguments[1]):s.call(e,~~arguments[0].left,~~arguments[0].top)},e.scrollBy=function(){if(a(arguments[0]))return p.call(e,arguments[0].left||arguments[0],arguments[0].top||arguments[1]);var t=e.scrollX||e.pageXOffset,r=e.scrollY||e.pageYOffset;return s(~~arguments[0].left+t,~~arguments[0].top+r)},Element.prototype.scrollIntoView=function(){var t,r,n,o,l,s;return a(arguments[0])?g.call(this,arguments[0]||!0):(l=i(this),l&&(s=e.getComputedStyle(l,null),r=parseInt(s.getPropertyValue("padding-left"),10),n=parseInt(s.getPropertyValue("padding-top"),10),t={top:this.offsetTop-2*n,left:this.offsetLeft-2*r},o=u(l,t)),o)}}}(window,document);
+!function(e,t,r){"use strict";function n(){return e.performance!==r&&e.performance.now!==r?e.performance.now():Date.now()}function o(e){return.5*(1-Math.cos(Math.PI*e))}function a(e){if("object"!=typeof e||e.behavior===r||"auto"===e.behavior||"instant"===e.behavior)return!0;if("smooth"===e.behavior)return!1;throw new TypeError(e.behavior+" is not a valid value for enumeration ScrollBehavior")}function l(e,t,r){e.scrollTop=r,e.scrollLeft=t}function i(t,a){function l(){var p,g,v,h=n(),d=(h-m)/c;return d=d>1?1:d,p=o(d),g=i+(t-i)*p,v=u+(a-u)*p,f(g,v),g===t&&v===a?(i=u=m=null,e.cancelAnimationFrame(s),r):(s=e.requestAnimationFrame(l),r)}var i=e.scrollX||e.pageXOffset,u=e.scrollY||e.pageYOffset,m=n();s&&e.cancelAnimationFrame(s),s=e.requestAnimationFrame(l)}function u(a,u){function f(){var t,i,u,d=n(),y=(d-h)/c;return y=y>1?1:y,t=o(y),i=m+(g-m)*t,u=p+(v-p)*t,l(a,i,u),i===g&&u===v?(m=p=h=null,e.cancelAnimationFrame(s),r):(s=e.requestAnimationFrame(f),r)}if(a===t.documentElement||a===t.body)return i(u.left,u.top),r;var m=a.scrollLeft,p=a.scrollTop,g=u.left,v=u.top,h=n();s&&e.cancelAnimationFrame(s),s=e.requestAnimationFrame(f)}if(!("scrollBehavior"in t.documentElement.style)){var s,c=768,f=e.scrollTo,m=e.scrollBy,p=e.Element.prototype.scrollIntoView;e.scroll=e.scrollTo=function(){return a(arguments[0])?f.call(e,arguments[0].left||arguments[0],arguments[0].top||arguments[1]):i.call(e,~~arguments[0].left,~~arguments[0].top)},e.scrollBy=function(){if(a(arguments[0]))return m.call(e,arguments[0].left||arguments[0],arguments[0].top||arguments[1]);var t=e.scrollX||e.pageXOffset,r=e.scrollY||e.pageYOffset;return i(~~arguments[0].left+t,~~arguments[0].top+r)},Element.prototype.scrollIntoView=function(){var r,n,o,l;return a(arguments[0])?p.call(this,arguments[0]||!0):(l=e.getComputedStyle(t.body,null),n=parseInt(l.getPropertyValue("padding-left"),10),o=parseInt(l.getPropertyValue("padding-top"),10),r={top:this.offsetTop-2*o,left:this.offsetLeft-2*n},u(t.body,r))}}}(window,document);
index 0ed109243d5060eb3af7c39f6a69d4c13f95c0d2..33eb3e8bb6932fffb4d23d7f2daf0df1ce631203 100644 (file)
@@ -310,8 +310,8 @@ WCF.Message.Preview = Class.extend({
                
                // validate message field
                this._messageFieldID = $.wcfEscapeID(messageFieldID);
-               this._messageField = $('#' + this._messageFieldID);
-               if (!this._messageField.length) {
+               this._textarea = $('#' + this._messageFieldID);
+               if (!this._textarea.length) {
                        console.debug("[WCF.Message.Preview] Unable to find message field identified by '" + this._messageFieldID + "'");
                        return;
                }
@@ -389,10 +389,10 @@ WCF.Message.Preview = Class.extend({
         */
        _getMessage: function() {
                if (!$.browser.redactor) {
-                       return $.trim(this._messageField.val());
+                       return $.trim(this._textarea.val());
                }
-               else if (this._messageField.data('redactor')) {
-                       return this._messageField.redactor('wutil.getText');
+               else if (this._textarea.data('redactor')) {
+                       return this._textarea.redactor('wutil.getText');
                }
                
                return null;
@@ -410,7 +410,7 @@ WCF.Message.Preview = Class.extend({
                this._previewButton.html(this._previewButtonLabel).enable();
                
                // remove error message
-               this._messageField.parent().children('small.innerError').remove();
+               this._textarea.parent().children('small.innerError').remove();
                
                // evaluate message
                this._handleResponse(data);
@@ -439,9 +439,9 @@ WCF.Message.Preview = Class.extend({
                // restore preview button
                this._previewButton.html(this._previewButtonLabel).enable();
                
-               var $innerError = this._messageField.next('small.innerError').empty();
+               var $innerError = this._textarea.next('small.innerError').empty();
                if (!$innerError.length) {
-                       $innerError = $('<small class="innerError" />').appendTo(this._messageField.parent());
+                       $innerError = $('<small class="innerError" />').appendTo(this._textarea.parent());
                }
                
                $innerError.html(data.returnValues.errorType);
@@ -722,492 +722,6 @@ WCF.Message.Smilies = Class.extend({
        }
 });
 
-/**
- * Provides an AJAX-based quick reply for messages.
- */
-WCF.Message.QuickReply = Class.extend({
-       /**
-        * quick reply container
-        * @var jQuery
-        */
-       _container: null,
-       
-       /**
-        * message field
-        * @var jQuery
-        */
-       _messageField: null,
-       
-       /**
-        * notification object
-        * @var WCF.System.Notification
-        */
-       _notification: null,
-       
-       /**
-        * true, if a request to save the message is pending
-        * @var boolean
-        */
-       _pendingSave: false,
-       
-       /**
-        * action proxy
-        * @var WCF.Action.Proxy
-        */
-       _proxy: null,
-       
-       /**
-        * collection of quick reply buttons
-        * @var jQuery
-        */
-       _quickReplyButtons: null,
-       
-       /**
-        * quote manager object
-        * @var WCF.Message.Quote.Manager
-        */
-       _quoteManager: null,
-       
-       /**
-        * scroll handler
-        * @var WCF.Effect.Scroll
-        */
-       _scrollHandler: null,
-       
-       /**
-        * success message for created but invisible messages
-        * @var string
-        */
-       _successMessageNonVisible: '',
-       
-       /**
-        * Initializes a new WCF.Message.QuickReply object.
-        * 
-        * @param       boolean                         supportExtendedForm
-        * @param       WCF.Message.Quote.Manager       quoteManager
-        */
-       init: function(supportExtendedForm, quoteManager) {
-               this._container = $('#messageQuickReply');
-               this._container.children('.message').addClass('jsInvalidQuoteTarget');
-               this._messageField = $('#text');
-               this._pendingSave = false;
-               if (!this._container || !this._messageField) {
-                       return;
-               }
-               
-               // button actions
-               var $formSubmit = this._container.find('.formSubmit');
-               var $saveButton = $formSubmit.find('button[data-type=save]').removeAttr('accesskey').click($.proxy(this._save, this));
-               if (supportExtendedForm) $formSubmit.find('button[data-type=extended]').click($.proxy(this._prepareExtended, this));
-               $formSubmit.find('button[data-type=cancel]').click($.proxy(this._cancel, this));
-               
-               if (quoteManager) this._quoteManager = quoteManager;
-               
-               this._quickReplyButtons = $('.jsQuickReply').data('__api', this).click($.proxy(this.click, this));
-               
-               this._proxy = new WCF.Action.Proxy({
-                       failure: $.proxy(this._failure, this),
-                       showLoadingOverlay: false,
-                       success: $.proxy(this._success, this)
-               });
-               this._scroll = new WCF.Effect.Scroll();
-               this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.add'));
-               this._successMessageNonVisible = '';
-               
-               WCF.System.Event.addListener('com.woltlab.wcf.redactor', 'submitEditor_text', function(data) {
-                       data.cancel = true;
-                       
-                       $saveButton.trigger('click');
-               });
-       },
-       
-       /**
-        * Handles clicks on reply button.
-        * 
-        * @param       object          event
-        */
-       click: function(event) {
-               this._container.toggle();
-               
-               if (this._container.is(':visible')) {
-                       this._quickReplyButtons.each(function() {
-                               var $button = $(this);
-                               if ($button.parent()[0].tagName === 'LI') {
-                                       $button.parent().hide();
-                               }
-                               else {
-                                       $button.hide();
-                               }
-                       });
-                       
-                       setTimeout((function() {
-                               $(document).trigger('resize');
-                               if (!$.browser.mobile || !$.browser.chrome) {
-                                       // Chrome on Android scrolls to the caret position, manually scrolling breaks the position
-                                       this._scroll.scrollTo(this._container, true);
-                               }
-                       }).bind(this), 100);
-                       
-                       WCF.Message.Submit.registerButton('text', this._container.find('.formSubmit button[data-type=save]'));
-                       
-                       if (this._quoteManager) {
-                               // check if message field is empty
-                               var $empty = true;
-                               if ($.browser.redactor) {
-                                       if (this._messageField.data('redactor')) {
-                                               this._editorCallback(this._messageField.redactor('wutil.isEmptyEditor'));
-                                       }
-                               }
-                               else {
-                                       $empty = (!this._messageField.val().length);
-                                       this._editorCallback($empty);
-                               }
-                       }
-               }
-               
-               // discard event
-               if (event !== null) {
-                       event.stopPropagation();
-                       return false;
-               }
-       },
-       
-       /**
-        * Inserts quotes and focuses the editor.
-        */
-       _editorCallback: function(isEmpty) {
-               if (isEmpty) {
-                       this._quoteManager.insertQuotes(this._getClassName(), this._getObjectID(), $.proxy(this._insertQuotes, this));
-               }
-               
-               if ($.browser.redactor) {
-                       this._messageField.redactor('wutil.selectionEndOfEditor');
-               }
-               else {
-                       this._messageField.focus();
-               }
-       },
-       
-       /**
-        * Returns container element.
-        * 
-        * @return      jQuery
-        */
-       getContainer: function() {
-               return this._container;
-       },
-       
-       /**
-        * Insertes quotes into the quick reply editor.
-        * 
-        * @param       object          data
-        */
-       _insertQuotes: function(data) {
-               if (!data.returnValues.template) {
-                       return;
-               }
-               
-               if ($.browser.redactor) {
-                       var $html = WCF.String.unescapeHTML(data.returnValues.template);
-                       $html = this._messageField.redactor('wbbcode.convertToHtml', $html);
-                       $html = $html.replace(/<p><blockquote/, '<blockquote');
-                       $html = $html.replace(/blockquote><\/p>/, 'blockquote>');
-                       
-                       this._messageField.redactor('focus.setEnd');
-                       this._messageField.redactor('wutil.insertDynamic', $html, data.returnValues.template);
-                       this._messageField.redactor('wutil.selectionEndOfEditor');
-                       this._messageField.redactor('wbbcode.observeQuotes');
-               }
-               else {
-                       this._messageField.val(data.returnValues.template);
-               }
-       },
-       
-       /**
-        * Saves message.
-        */
-       _save: function() {
-               if (this._pendingSave) {
-                       return;
-               }
-               
-               var $message = '';
-               if ($.browser.redactor) {
-                       $message = this._messageField.redactor('wutil.getText');
-               }
-               else {
-                       $message = $.trim(this._messageField.val());
-               }
-               
-               // check if message is empty
-               var $innerError = this._messageField.parent().find('small.innerError');
-               if ($message === '' || $message === '0') {
-                       if (!$innerError.length) {
-                               $innerError = $('<small class="innerError" />').appendTo(this._messageField.parent());
-                       }
-                       
-                       $innerError.html(WCF.Language.get('wcf.global.form.error.empty'));
-                       return;
-               }
-               else {
-                       $innerError.remove();
-               }
-               
-               this._pendingSave = true;
-               
-               this._proxy.setOption('data', {
-                       actionName: 'quickReply',
-                       className: this._getClassName(),
-                       interfaceName: 'wcf\\data\\IMessageQuickReplyAction',
-                       parameters: this._getParameters($message)
-               });
-               this._proxy.sendRequest();
-               
-               // show spinner and hide Redactor
-               var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
-               $('<span class="icon icon48 fa-spinner" />').appendTo($messageBody);
-               var $redactorBox = $messageBody.children('.redactor-box').hide();
-               
-               // hide message tabs
-               $redactorBox.next().hide();
-               
-               // hide form submit
-               $messageBody.next().hide();
-       },
-       
-       /**
-        * Returns the parameters for the save request.
-        * 
-        * @param       string          message
-        * @return      object
-        */
-       _getParameters: function(message) {
-               var $parameters = {
-                       objectID: this._getObjectID(),
-                       data: {
-                               message: message
-                       },
-                       lastPostTime: this._container.data('lastPostTime'),
-                       pageNo: this._container.data('pageNo'),
-                       removeQuoteIDs: (this._quoteManager === null ? [ ] : this._quoteManager.getQuotesMarkedForRemoval())
-               };
-               if (this._container.data('anchor')) {
-                       $parameters.anchor = this._container.data('anchor');
-               }
-               
-               WCF.System.Event.fireEvent('com.woltlab.wcf.messageOptionsInline', 'submit_' + this._messageField.wcfIdentify(), $parameters.data);
-               
-               return $parameters;
-       },
-       
-       /**
-        * Cancels quick reply.
-        */
-       _cancel: function() {
-               this._revertQuickReply(true);
-               
-               if ($.browser.redactor) {
-                       this._messageField.redactor('wutil.reset');
-               }
-               else {
-                       this._messageField.val('');
-               }
-       },
-       
-       /**
-        * Reverts quick reply to original state and optionally hiding it.
-        * 
-        * @param       boolean         hide
-        */
-       _revertQuickReply: function(hide) {
-               var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
-               
-               if (hide) {
-                       this._container.hide();
-                       
-                       // remove previous error messages
-                       $messageBody.children('small.innerError').remove();
-               }
-               
-               // display Redactor
-               $messageBody.children('.fa-spinner').remove();
-               $messageBody.children('.redactor-box').show().next().show();
-               
-               // display form submit
-               $messageBody.next().show();
-               
-               this._quickReplyButtons.each(function() {
-                       var $button = $(this);
-                       if ($button.parent()[0].tagName === 'LI') {
-                               $button.parent().show();
-                       }
-                       else {
-                               $button.show();
-                       }
-               });
-       },
-       
-       /**
-        * Prepares jump to extended message add form.
-        */
-       _prepareExtended: function() {
-               this._pendingSave = true;
-               
-               // mark quotes for removal
-               if (this._quoteManager !== null) {
-                       this._quoteManager.markQuotesForRemoval();
-               }
-               
-               var $message = '';
-               if ($.browser.redactor) {
-                       $message = this._messageField.redactor('wutil.getText');
-                       
-                       if ($message.length) {
-                               this._messageField.redactor('wutil.saveTextToStorage', true);
-                       }
-                       else {
-                               this._messageField.redactor('wutil.autosavePurge');
-                       }
-               }
-               else {
-                       $message = $.trim(this._messageField.val());
-               }
-               
-               var $parameters = {
-                       containerID: this._getObjectID(),
-                       message: $message
-               };
-               
-               WCF.System.Event.fireEvent('com.woltlab.wcf.messageOptionsInline', 'prepareExtended_' + this._messageField.wcfIdentify(), $parameters);
-               
-               new WCF.Action.Proxy({
-                       autoSend: true,
-                       data: {
-                               actionName: 'jumpToExtended',
-                               className: this._getClassName(),
-                               interfaceName: 'wcf\\data\\IExtendedMessageQuickReplyAction',
-                               parameters: $parameters
-                       },
-                       success: (function(data) {
-                               this._messageField.redactor('wutil.saveTextToStorage');
-                               window.location = data.returnValues.url;
-                       }).bind(this)
-               });
-       },
-       
-       /**
-        * Handles successful AJAX calls.
-        * 
-        * @param       object          data
-        * @param       string          textStatus
-        * @param       jQuery          jqXHR
-        */
-       _success: function(data, textStatus, jqXHR) {
-               if ($.browser.redactor) {
-                       this._messageField.redactor('wutil.autosavePause');
-                       this._messageField.redactor('wutil.autosavePurge');
-               }
-               
-               // redirect to new page
-               if (data.returnValues.url) {
-                       window.location = data.returnValues.url;
-               }
-               else {
-                       if (data.returnValues.template) {
-                               // insert HTML
-                               var $message = $('' + data.returnValues.template);
-                               if (this._container.data('sortOrder') == 'DESC') {
-                                       $message.insertAfter(this._container);
-                               }
-                               else {
-                                       $message.insertBefore(this._container);
-                               }
-                               
-                               // update last post time
-                               this._container.data('lastPostTime', data.returnValues.lastPostTime);
-                               
-                               // show notification
-                               this._notification.show(undefined, undefined, WCF.Language.get('wcf.global.success.add'));
-                               
-                               this._updateHistory($message.wcfIdentify());
-                       }
-                       else {
-                               // show notification
-                               var $message = (this._successMessageNonVisible) ? this._successMessageNonVisible : 'wcf.global.success.add';
-                               this._notification.show(undefined, 5000, WCF.Language.get($message));
-                       }
-                       
-                       if ($.browser.redactor) {
-                               this._messageField.redactor('wutil.reset');
-                               this._messageField.redactor('wutil.autosaveResume');
-                       }
-                       else {
-                               this._messageField.val('');
-                       }
-                       
-                       // hide quick reply and revert it
-                       this._revertQuickReply(true);
-                       
-                       // count stored quotes
-                       if (this._quoteManager !== null) {
-                               this._quoteManager.countQuotes();
-                       }
-                       
-                       this._pendingSave = false;
-               }
-       },
-       
-       /**
-        * Reverts quick reply on failure to preserve entered message.
-        */
-       _failure: function(data) {
-               this._pendingSave = false;
-               this._revertQuickReply(false);
-               
-               if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
-                       return true;
-               }
-               
-               var $messageBody = this._container.find('.messageQuickReplyContent .messageBody');
-               var $innerError = $messageBody.children('small.innerError').empty();
-               if (!$innerError.length) {
-                       $innerError = $('<small class="innerError" />').appendTo($messageBody);
-               }
-               
-               $innerError.html(data.returnValues.errorType);
-               
-               return false;
-       },
-       
-       /**
-        * Returns action class name.
-        * 
-        * @return      string
-        */
-       _getClassName: function() {
-               return '';
-       },
-       
-       /**
-        * Returns object id.
-        * 
-        * @return      integer
-        */
-       _getObjectID: function() {
-               return 0;
-       },
-       
-       /**
-        * Updates the history to avoid old content when going back in the browser
-        * history.
-        * 
-        * @param       hash
-        */
-       _updateHistory: function(hash) {
-               window.location.hash = hash;
-       }
-});
-
 /**
  * Provides an inline message editor.
  * 
index d2acaae6c9f63f7b213d3e1d956202825188213f..d96e7c977498a0e7b84ea692eec30a55e6fe8e78 100644 (file)
@@ -6,7 +6,7 @@
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module     WoltLab/WCF/Dom/Util
  */
-define([], function() {
+define(['StringUtil'], function(StringUtil) {
        "use strict";
        
        var _matchesSelectorFunction = '';
@@ -186,7 +186,7 @@ define([], function() {
                 * Applies a list of CSS properties to an element.
                 * 
                 * @param       {Element}               el      element
-                * @param       {Object<string, mixed>} styles  list of CSS styles
+                * @param       {Object<string, *>}     styles  list of CSS styles
                 */
                setStyles: function(el, styles) {
                        var important = false;
@@ -214,7 +214,7 @@ define([], function() {
                 * 
                 * @param       {CSSStyleDeclaration}   styles          result of window.getComputedStyle()
                 * @param       {string}                propertyName    property name
-                * @return      {integer}       property value as integer
+                * @return      {int}                   property value as integer
                 */
                styleAsInt: function(styles, propertyName) {
                        var value = styles.getPropertyValue(propertyName);
@@ -251,6 +251,41 @@ define([], function() {
                        }
                },
                
+               /**
+                * 
+                * @param html
+                * @param {Element} referenceElement
+                * @param insertMethod
+                */
+               insertHtml: function(html, referenceElement, insertMethod) {
+                       var element = elCreate('div');
+                       this.setInnerHtml(element, html);
+                       
+                       if (insertMethod === 'append' || insertMethod === 'after') {
+                               while (element.childNodes.length) {
+                                       if (insertMethod === 'append') {
+                                               referenceElement.appendChild(element.childNodes[0]);
+                                       }
+                                       else {
+                                               this.insertAfter(element.childNodes[0], referenceElement);
+                                       }
+                               }
+                       }
+                       else if (insertMethod === 'prepend' || insertMethod === 'before') {
+                               for (var i = element.childNodes.length - 1; i >= 0; i--) {
+                                       if (insertMethod === 'prepend') {
+                                               this.prepend(element.childNodes[i], referenceElement);
+                                       }
+                                       else {
+                                               referenceElement.parentNode.insertBefore(element.childNodes[i], referenceElement);
+                                       }
+                               }
+                       }
+                       else {
+                               throw new Error("Unknown insert method '" + insertMethod + "'.");
+                       }
+               },
+               
                /**
                 * Returns true if `element` contains the `child` element.
                 * 
@@ -268,6 +303,53 @@ define([], function() {
                        }
                        
                        return false;
+               },
+               
+               /**
+                * Retrieves all data attributes from target element, optionally allowing for
+                * a custom prefix that serves two purposes: First it will restrict the results
+                * for items starting with it and second it will remove that prefix.
+                * 
+                * @param       {Element}       element         target element
+                * @param       {string=}       prefix          attribute prefix
+                * @param       {boolean=}      camcelCaseName  transform attribute names into camel case using dashes as separators
+                * @param       {boolean=}      idToUpperCase   transform '-id' into 'ID'
+                * @returns     {object<string, string>}        list of data attributes
+                */
+               getDataAttributes: function(element, prefix, camcelCaseName, idToUpperCase) {
+                       prefix = prefix || '';
+                       if (!/^data-/.test(prefix)) prefix = 'data-' + prefix;
+                       camcelCaseName = (camcelCaseName === true);
+                       idToUpperCase = (idToUpperCase === true);
+                       
+                       var attribute, attributes = {}, name, tmp;
+                       for (var i = 0, length = element.attributes.length; i < length; i++) {
+                               attribute = element.attributes[i];
+                               
+                               if (attribute.name.indexOf(prefix) === 0) {
+                                       name = attribute.name.replace(new RegExp('^' + prefix), '');
+                                       if (camcelCaseName) {
+                                               tmp = name.split('-');
+                                               name = '';
+                                               for (var j = 0, innerLength = tmp.length; j < innerLength; j++) {
+                                                       if (name.length) {
+                                                               if (idToUpperCase && tmp[j] === 'id') {
+                                                                       tmp[j] = 'ID';
+                                                               }
+                                                               else {
+                                                                       tmp[j] = StringUtil.ucfirst(tmp[j]);
+                                                               }
+                                                       }
+                                                       
+                                                       name += tmp[j];
+                                               }
+                                       }
+                                       
+                                       attributes[name] = attribute.value;
+                               }
+                       }
+                       
+                       return attributes;
                }
        };
        
index c65f09e6bb0405812848411ae1493b425a961e16..13ff7b5634c5cad307138fd00e2500ad7609ece8 100644 (file)
@@ -14,7 +14,7 @@ define(['Core', 'Dictionary'], function(Core, Dictionary) {
        /**
         * @exports     WoltLab/WCF/Event/Handler
         */
-       var EventHandler = {
+       return {
                /**
                 * Adds an event listener.
                 * 
@@ -111,6 +111,4 @@ define(['Core', 'Dictionary'], function(Core, Dictionary) {
                        }
                }
        };
-       
-       return EventHandler;
 });
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js
new file mode 100644 (file)
index 0000000..0b84ebc
--- /dev/null
@@ -0,0 +1,276 @@
+/**
+ * Handles user interaction with the quick reply feature.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Ui/Message/Reply
+ */
+define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/Util', 'Ui/Notification', '../Scroll'], function(Ajax, Core, EventHandler, Language, DomUtil, UiNotification, UiScroll) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function UiMessageReply(options) { this.init(options); }
+       UiMessageReply.prototype = {
+               /**
+                * Initializes a new quick reply field.
+                * 
+                * @param       {Object}        options         configuration options
+                */
+               init: function(options) {
+                       this._options = Core.extend({
+                               ajax: {
+                                       className: ''
+                               },
+                               successMessage: 'wcf.global.success.add'
+                       }, options);
+                       
+                       this._container = elById('messageQuickReply');
+                       this._content = elBySel('.messageContent', this._container);
+                       this._textarea = elById('text');
+                       this._editor = null;
+                       this._loadingOverlay = null;
+                       
+                       // prevent marking of text for quoting
+                       elBySel('.message', this._container).classList.add('jsInvalidQuoteTarget');
+                       
+                       // handle submit button
+                       var submitCallback = this._submit.bind(this);
+                       var submitButton = elBySel('button[data-type="save"]');
+                       submitButton.addEventListener(WCF_CLICK_EVENT, submitCallback);
+                       
+                       // bind reply button
+                       var replyButtons = elBySelAll('.jsQuickReply');
+                       for (var i = 0, length = replyButtons.length; i < length; i++) {
+                               replyButtons[i].addEventListener(WCF_CLICK_EVENT, (function(event) {
+                                       event.preventDefault();
+                                       
+                                       UiScroll.element(this._container, (function() {
+                                               this._getEditor().focus.end();
+                                       }).bind(this));
+                               }).bind(this));
+                       }
+                       
+                       // TODO: add event listener for submit through keyboard in Redactor
+               },
+               
+               /**
+                * Validates the message and submits it to the server.
+                * 
+                * @param       {Event}         event   event object
+                * @protected
+                */
+               _submit: function(event) {
+                       event.preventDefault();
+                       
+                       if (!this._validate()) {
+                               // validation failed, bail out
+                               return;
+                       }
+                       
+                       this._showLoadingOverlay();
+                       
+                       // build parameters
+                       var parameters = DomUtil.getDataAttributes(this._container, 'data-', true, true);
+                       parameters.data = { message: this._getEditor().code.get() };
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_text', parameters);
+                       
+                       Ajax.api(this, {
+                               parameters: parameters
+                       });
+               },
+               
+               /**
+                * Validates the message and invokes listeners to perform additional validation.
+                * 
+                * @return      {boolean}       validation result
+                * @protected
+                */
+               _validate: function() {
+                       // remove all existing error elements
+                       var errorMessages = elByClass('innerError', this._container);
+                       while (errorMessages.length) {
+                               errorMessages[0].parentNode.removeChild(errorMessages[0]);
+                       }
+                       
+                       // check if editor contains actual content
+                       if (this._getEditor().utils.isEmpty()) {
+                               this.throwError(this._textarea, Language.get('wcf.global.form.error.empty'));
+                               return false;
+                       }
+                       
+                       var data = {
+                               'api': this,
+                               'editor': this._getEditor(),
+                               'message': this._getEditor().code.get(),
+                               'valid': true
+                       };
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_text', data);
+                       
+                       return (data.valid !== false);
+               },
+               
+               /**
+                * Throws an error by adding an inline error to target element.
+                * 
+                * @param       {Element}       element         erroneous element
+                * @param       {string}        message         error message
+                */
+               throwError: function(element, message) {
+                       var error = elCreate('small');
+                       error.className = 'innerError';
+                       error.textContent = message;
+                       
+                       DomUtil.insertAfter(error, element);
+               },
+               
+               /**
+                * Displays a loading spinner while the request is processed by the server.
+                * 
+                * @protected
+                */
+               _showLoadingOverlay: function() {
+                       if (this._loadingOverlay === null) {
+                               this._loadingOverlay = elCreate('div');
+                               this._loadingOverlay.className = 'messageContentLoadingOverlay';
+                               this._loadingOverlay.innerHTML = '<span class="icon icon96 fa-spinner"></span>';
+                       }
+                       
+                       this._content.classList.add('loading');
+                       this._content.appendChild(this._loadingOverlay);
+               },
+               
+               /**
+                * Hides the loading spinner.
+                * 
+                * @protected
+                */
+               _hideLoadingOverlay: function() {
+                       this._content.classList.remove('loading');
+                       
+                       var loadingOverlay = elBySel('.messageContentLoadingOverlay', this._content);
+                       if (loadingOverlay !== null) {
+                               loadingOverlay.parentNode.removeChild(loadingOverlay);
+                       }
+               },
+               
+               /**
+                * Resets the editor contents and notifies event listeners.
+                * 
+                * @protected
+                */
+               _reset: function() {
+                       this._getEditor().code.set('<p>\u200b</p>');
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'reset_text');
+               },
+               
+               /**
+                * Handles errors occured during server processing.
+                * 
+                * @param       {Object}        data    response data
+                * @protected
+                */
+               _handleError: function(data) {
+                       this.throwError(this._textarea, data.returnValues.errorType);
+               },
+               
+               /**
+                * Returns the current editor instance.
+                * 
+                * @return      {Object}       editor instance
+                * @protected
+                */
+               _getEditor: function() {
+                       if (this._editor === null) {
+                               if (typeof window.jQuery === 'function') {
+                                       this._editor = window.jQuery(this._textarea).data('redactor');
+                               }
+                               else {
+                                       throw new Error("Unable to access editor, jQuery has not been loaded yet.");
+                               }
+                       }
+                       
+                       return this._editor;
+               },
+               
+               /**
+                * Inserts the rendered message into the post list, unless the post is on the next
+                * page in which case a redirect will be performed instead.
+                * 
+                * @param       {Object}        data    response data
+                * @protected
+                */
+               _insertMessage: function(data) {
+                       // TODO: clear autosave content and disable it
+                       
+                       // redirect to new page
+                       if (data.returnValues.url) {
+                               window.location = data.returnValues.url;
+                       }
+                       else {
+                               if (data.returnValues.template) {
+                                       var elementId;
+                                       
+                                       // insert HTML
+                                       if (elData(this._container, 'sort-order') === 'DESC') {
+                                               DomUtil.insertHtml(data.returnValues.template, this._container, 'after');
+                                               elementId = DomUtil.identify(this._container.nextElementSibling);
+                                       }
+                                       else {
+                                               DomUtil.insertHtml(data.returnValues.template, this._container, 'before');
+                                               elementId = DomUtil.identify(this._container.previousElementSibling);
+                                       }
+                                       
+                                       // update last post time
+                                       elData(this._container, 'last-post-time', data.returnValues.lastPostTime);
+                                       
+                                       window.location.hash = elementId;
+                                       UiScroll.element(elById(elementId));
+                               }
+                               
+                               UiNotification.show(Language.get(this._options.successMessage));
+                               
+                               // TODO: resume autosave
+                               
+                               // TODO: reload quotes
+                       }
+               },
+               
+               _ajaxSuccess: function(data) {
+                       this._insertMessage(data);
+                       
+                       this._reset();
+                       
+                       this._hideLoadingOverlay();
+               },
+               
+               _ajaxFailure: function(data) {
+                       this._hideLoadingOverlay();
+                       
+                       if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
+                               return true;
+                       }
+                       
+                       this._handleError(data);
+                       
+                       return false;
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'quickReply',
+                                       className: this._options.ajax.className,
+                                       interfaceName: 'wcf\\data\\IMessageQuickReplyAction'
+                               }
+                       };
+               }
+       };
+       
+       return UiMessageReply;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Scroll.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Scroll.js
new file mode 100644 (file)
index 0000000..ff759d8
--- /dev/null
@@ -0,0 +1,84 @@
+/**
+ * Smoothly scrolls to an element while accounting for potential sticky headers.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Ui/Scroll
+ */
+define(['Dom/Util'], function(DomUtil) {
+       "use strict";
+       
+       var _callback = null;
+       var _callbackScroll = null;
+       var _timeoutScroll = null;
+       
+       /**
+        * @exports     WoltLab/WCF/Ui/Scroll
+        */
+       return {
+               /**
+                * Scrolls to target element, optionally invoking the provided callback once scrolling has ended.
+                * 
+                * @param       {Element}       element         target element
+                * @param       {function=}     callback        callback invoked once scrolling has ended
+                */
+               element: function(element, callback) {
+                       if (!(element instanceof Element)) {
+                               throw new TypeError("Expected a valid DOM element.");
+                       }
+                       else if (callback !== undefined && typeof callback !== 'function') {
+                               throw new TypeError("Expected a valid callback function.");
+                       }
+                       else if (!document.body.contains(element)) {
+                               throw new Error("Element must be part of the visible DOM.");
+                       }
+                       else if (_callback !== null) {
+                               throw new Error("Cannot scroll to element, a concurrent request is running.");
+                       }
+                       
+                       if (callback) {
+                               _callback = callback;
+                               
+                               if (_callbackScroll === null) {
+                                       _callbackScroll = this._onScroll.bind(this);
+                               }
+                               
+                               window.addEventListener('scroll', _callbackScroll);
+                       }
+                       
+                       var y = DomUtil.offset(element).top;
+                       
+                       if (y <= 50) {
+                               y = 0;
+                       }
+                       else {
+                               // add an offset of 50 pixel to account for a sticky header
+                               y -= 50;
+                       }
+                       
+                       window.scrollTo({
+                               left: 0,
+                               top: y,
+                               behavior: 'smooth'
+                       });
+               },
+               
+               /**
+                * Monitors scroll event to only execute the callback once scrolling has ended.
+                * 
+                * @protected
+                */
+               _onScroll: function() {
+                       if (_timeoutScroll !== null) window.clearTimeout(_timeoutScroll);
+                       
+                       _timeoutScroll = window.setTimeout(function() {
+                               _callback();
+                               
+                               window.removeEventListener('scroll', _callbackScroll);
+                               _callback = null;
+                               _timeoutScroll = null;
+                       }, 100);
+               }
+       }
+});
index a8db731b623e3baec1ae7cc44b62de4e58855809..b0ac6a8b7ad887d7c6d0dbfc7dcf488c65c68dbf 100644 (file)
        window.objOwns = function(obj, property) {
                return obj.hasOwnProperty(property);
        };
+       
+       /* assigns a global constant defining the proper 'click' event depending on the browser,
+          enforcing 'touchstart' on mobile devices for a better UX. We're using defineProperty()
+          here because at the time of writing Safari does not support 'const'. Thanks Safari.
+        */
+       var clickEvent = ('touchstart' in document.documentElement) ? 'touchstart' : 'click';
+       Object.defineProperty(window, 'WCF_CLICK_EVENT', {
+               value: clickEvent
+       });
 })(window, document);
index c87b622ca369c57f0799e8c1f666188984f35eed..2dc37b2a4d6a0b4425ac37109157af4b56c8ecac 100644 (file)
        flex: 1 auto;
        flex-direction: column;
        margin-left: 30px;
+       
+       &.loading {
+               position: relative;
+               
+               > .messageContentLoadingOverlay {
+                       align-items: center;
+                       background-color: $wcfContentBackground;
+                       bottom: 0;
+                       display: flex;
+                       justify-content: center;
+                       left: 0;
+                       position: absolute;
+                       right: 0;
+                       top: 0;
+                       
+                       > .icon {
+                               flex: 0 0 auto;
+                       }
+               }
+       }
 }
 
 /* content - header */