From 9d118fa4c035827b399acdcb4c6c05ad1be5f3c5 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 31 Jan 2016 13:46:41 +0100 Subject: [PATCH] Replaced WCF.M;essage.QuickReply --- wcfsetup/install/files/js/WCF.Assets.js | 4 +- wcfsetup/install/files/js/WCF.Message.js | 502 +----------------- .../install/files/js/WoltLab/WCF/Dom/Util.js | 88 ++- .../files/js/WoltLab/WCF/Event/Handler.js | 4 +- .../files/js/WoltLab/WCF/Ui/Message/Reply.js | 276 ++++++++++ .../install/files/js/WoltLab/WCF/Ui/Scroll.js | 84 +++ wcfsetup/install/files/js/wcf.globalHelper.js | 9 + wcfsetup/install/files/style/ui/message.scss | 20 + 8 files changed, 486 insertions(+), 501 deletions(-) create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/Ui/Scroll.js diff --git a/wcfsetup/install/files/js/WCF.Assets.js b/wcfsetup/install/files/js/WCF.Assets.js index 145db8d8bf..04f5e47544 100644 --- a/wcfsetup/install/files/js/WCF.Assets.js +++ b/wcfsetup/install/files/js/WCF.Assets.js @@ -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.clientHeight1?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); diff --git a/wcfsetup/install/files/js/WCF.Message.js b/wcfsetup/install/files/js/WCF.Message.js index 0ed109243d..33eb3e8bb6 100644 --- a/wcfsetup/install/files/js/WCF.Message.js +++ b/wcfsetup/install/files/js/WCF.Message.js @@ -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 = $('').appendTo(this._messageField.parent()); + $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>'); - - 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 = $('').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'); - $('').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 = $('').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. * diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Dom/Util.js b/wcfsetup/install/files/js/WoltLab/WCF/Dom/Util.js index d2acaae6c9..d96e7c9774 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Dom/Util.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Dom/Util.js @@ -6,7 +6,7 @@ * @license GNU Lesser General Public License * @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} styles list of CSS styles + * @param {Object} 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} 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; } }; diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js b/wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js index c65f09e6bb..13ff7b5634 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js @@ -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 index 0000000000..0b84ebc7a6 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js @@ -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 + * @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 = ''; + } + + 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('

\u200b

'); + + 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 index 0000000000..ff759d83ae --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Scroll.js @@ -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 + * @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); + } + } +}); diff --git a/wcfsetup/install/files/js/wcf.globalHelper.js b/wcfsetup/install/files/js/wcf.globalHelper.js index a8db731b62..b0ac6a8b7a 100644 --- a/wcfsetup/install/files/js/wcf.globalHelper.js +++ b/wcfsetup/install/files/js/wcf.globalHelper.js @@ -181,4 +181,13 @@ 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); diff --git a/wcfsetup/install/files/style/ui/message.scss b/wcfsetup/install/files/style/ui/message.scss index c87b622ca3..2dc37b2a4d 100644 --- a/wcfsetup/install/files/style/ui/message.scss +++ b/wcfsetup/install/files/style/ui/message.scss @@ -124,6 +124,26 @@ 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 */ -- 2.20.1