From 19e024286468e472505472522309787f592e6c4a Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 5 Nov 2020 23:20:36 +0100 Subject: [PATCH] Convert `Ui/Message/Replay` to TypeScript --- .../js/WoltLabSuite/Core/Ui/Message/Reply.js | 347 +++++++------- .../ts/WoltLabSuite/Core/Ui/Message/Reply.js | 434 ------------------ .../ts/WoltLabSuite/Core/Ui/Message/Reply.ts | 429 +++++++++++++++++ .../WoltLabSuite/Core/Ui/Redactor/Editor.ts | 9 +- 4 files changed, 592 insertions(+), 627 deletions(-) delete mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Reply.js create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Reply.ts diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js index 669626ffd2..c6a6dab9de 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js @@ -1,132 +1,109 @@ /** * Handles user interaction with the quick reply feature. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Ui/Message/Reply + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/Message/Reply */ -define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Dom/Traverse', 'Ui/Dialog', 'Ui/Notification', 'WoltLabSuite/Core/Ui/Scroll', 'EventKey', 'User', 'WoltLabSuite/Core/Controller/Captcha'], function (Ajax, Core, EventHandler, Language, DomChangeListener, DomUtil, DomTraverse, UiDialog, UiNotification, UiScroll, EventKey, User, ControllerCaptcha) { +define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Event/Handler", "../../Language", "../../Dom/Change/Listener", "../../Dom/Util", "../Dialog", "../Notification", "../../User", "../../Controller/Captcha", "../Scroll"], function (require, exports, tslib_1, Ajax, Core, EventHandler, Language, Listener_1, Util_1, Dialog_1, UiNotification, User_1, Captcha_1, UiScroll) { "use strict"; - if (!COMPILER_TARGET_DEFAULT) { - var Fake = function () { }; - Fake.prototype = { - init: function () { }, - _submitGuestDialog: function () { }, - _submit: function () { }, - _validate: function () { }, - throwError: function () { }, - _showLoadingOverlay: function () { }, - _hideLoadingOverlay: function () { }, - _reset: function () { }, - _handleError: function () { }, - _getEditor: function () { }, - _insertMessage: function () { }, - _ajaxSuccess: function () { }, - _ajaxFailure: function () { }, - _ajaxSetup: function () { } - }; - return Fake; - } - /** - * @constructor - */ - function UiMessageReply(options) { this.init(options); } - UiMessageReply.prototype = { + Ajax = tslib_1.__importStar(Ajax); + Core = tslib_1.__importStar(Core); + EventHandler = tslib_1.__importStar(EventHandler); + Language = tslib_1.__importStar(Language); + Listener_1 = tslib_1.__importDefault(Listener_1); + Util_1 = tslib_1.__importDefault(Util_1); + Dialog_1 = tslib_1.__importDefault(Dialog_1); + UiNotification = tslib_1.__importStar(UiNotification); + User_1 = tslib_1.__importDefault(User_1); + Captcha_1 = tslib_1.__importDefault(Captcha_1); + UiScroll = tslib_1.__importStar(UiScroll); + class UiMessageReply { /** * Initializes a new quick reply field. - * - * @param {Object} options configuration options */ - init: function (options) { + constructor(opts) { + this._editor = null; + this._guestDialogId = ""; + this._loadingOverlay = null; this._options = Core.extend({ ajax: { - className: '' + className: "", }, quoteManager: null, - successMessage: 'wcf.global.success.add' - }, options); - this._container = elById('messageQuickReply'); - this._content = elBySel('.messageContent', this._container); - this._textarea = elById('text'); - this._editor = null; - this._guestDialogId = ''; - this._loadingOverlay = null; + successMessage: "wcf.global.success.add", + }, opts); + this._container = document.getElementById("messageQuickReply"); + this._content = this._container.querySelector(".messageContent"); + this._textarea = document.getElementById("text"); // prevent marking of text for quoting - elBySel('.message', this._container).classList.add('jsInvalidQuoteTarget'); + this._container.querySelector(".message").classList.add("jsInvalidQuoteTarget"); // handle submit button - var submitCallback = this._submit.bind(this); - var submitButton = elBySel('button[data-type="save"]', this._container); - submitButton.addEventListener('click', submitCallback); + const submitButton = this._container.querySelector('button[data-type="save"]'); + submitButton.addEventListener("click", (ev) => this._submit(ev)); // bind reply button - var replyButtons = elBySelAll('.jsQuickReply'); - for (var i = 0, length = replyButtons.length; i < length; i++) { - replyButtons[i].addEventListener('click', (function (event) { + document.querySelectorAll(".jsQuickReply").forEach((replyButton) => { + replyButton.addEventListener("click", (event) => { event.preventDefault(); this._getEditor().WoltLabReply.showEditor(); - UiScroll.element(this._container, (function () { + UiScroll.element(this._container, () => { this._getEditor().WoltLabCaret.endOfEditor(); - }).bind(this)); - }).bind(this)); - } - }, + }); + }); + }); + } /** * Submits the guest dialog. - * - * @param {Event} event - * @protected */ - _submitGuestDialog: function (event) { + _submitGuestDialog(event) { // only submit when enter key is pressed - if (event.type === 'keypress' && !EventKey.Enter(event)) { + if (event instanceof KeyboardEvent && event.key !== "Enter") { return; } - var usernameInput = elBySel('input[name=username]', event.currentTarget.closest('.dialogContent')); - if (usernameInput.value === '') { - elInnerError(usernameInput, Language.get('wcf.global.form.error.empty')); - usernameInput.closest('dl').classList.add('formError'); + const target = event.currentTarget; + const dialogContent = target.closest(".dialogContent"); + const usernameInput = dialogContent.querySelector("input[name=username]"); + if (usernameInput.value === "") { + Util_1.default.innerError(usernameInput, Language.get("wcf.global.form.error.empty")); + usernameInput.closest("dl").classList.add("formError"); return; } - var parameters = { + let parameters = { parameters: { data: { - username: usernameInput.value - } - } + username: usernameInput.value, + }, + }, }; - //noinspection JSCheckFunctionSignatures - var captchaId = elData(event.currentTarget, 'captcha-id'); - if (ControllerCaptcha.has(captchaId)) { - var data = ControllerCaptcha.getData(captchaId); + const captchaId = target.dataset.captchaId; + if (Captcha_1.default.has(captchaId)) { + const data = Captcha_1.default.getData(captchaId); if (data instanceof Promise) { - data.then((function (data) { + void data.then((data) => { parameters = Core.extend(parameters, data); this._submit(undefined, parameters); - }).bind(this)); + }); } else { - parameters = Core.extend(parameters, ControllerCaptcha.getData(captchaId)); + parameters = Core.extend(parameters, Captcha_1.default.getData(captchaId)); this._submit(undefined, parameters); } } else { this._submit(undefined, parameters); } - }, + } /** * Validates the message and submits it to the server. - * - * @param {Event?} event event object - * @param {Object?} additionalParameters additional parameters sent to the server - * @protected */ - _submit: function (event, additionalParameters) { + _submit(event, additionalParameters) { if (event) { event.preventDefault(); } // Ignore requests to submit the message while a previous request is still pending. - if (this._content.classList.contains('loading')) { - if (!this._guestDialogId || !UiDialog.isOpen(this._guestDialogId)) { + if (this._content.classList.contains("loading")) { + if (!this._guestDialogId || !Dialog_1.default.isOpen(this._guestDialogId)) { return; } } @@ -136,236 +113,222 @@ define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/U } this._showLoadingOverlay(); // build parameters - var parameters = DomUtil.getDataAttributes(this._container, 'data-', true, true); + const parameters = {}; + Object.entries(this._container.dataset).forEach(([key, value]) => { + parameters[key.replace(/Id$/, "ID")] = value; + }); parameters.data = { message: this._getEditor().code.get() }; - parameters.removeQuoteIDs = (this._options.quoteManager) ? this._options.quoteManager.getQuotesMarkedForRemoval() : []; + parameters.removeQuoteIDs = this._options.quoteManager + ? this._options.quoteManager.getQuotesMarkedForRemoval() + : []; // add any available settings - var settingsContainer = elById('settings_text'); + const settingsContainer = document.getElementById("settings_text"); if (settingsContainer) { - elBySelAll('input, select, textarea', settingsContainer, function (element) { - if (element.nodeName === 'INPUT' && (element.type === 'checkbox' || element.type === 'radio')) { + settingsContainer + .querySelectorAll("input, select, textarea") + .forEach((element) => { + if (element.nodeName === "INPUT" && (element.type === "checkbox" || element.type === "radio")) { if (!element.checked) { return; } } - var name = element.name; - if (parameters.hasOwnProperty(name)) { - throw new Error("Variable overshadowing, key '" + name + "' is already present."); + const name = element.name; + if (Object.prototype.hasOwnProperty.call(parameters, name)) { + throw new Error(`Variable overshadowing, key '${name}' is already present.`); } parameters[name] = element.value.trim(); }); } - EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_text', parameters.data); - if (!User.userId && !additionalParameters) { + EventHandler.fire("com.woltlab.wcf.redactor2", "submit_text", parameters.data); + if (!User_1.default.userId && !additionalParameters) { parameters.requireGuestDialog = true; } Ajax.api(this, Core.extend({ - parameters: parameters + parameters: parameters, }, additionalParameters)); - }, + } /** * Validates the message and invokes listeners to perform additional validation. - * - * @return {boolean} validation result - * @protected */ - _validate: function () { + _validate() { // remove all existing error elements - elBySelAll('.innerError', this._container, elRemove); + this._container.querySelectorAll(".innerError").forEach((el) => el.remove()); // check if editor contains actual content if (this._getEditor().utils.isEmpty()) { - this.throwError(this._textarea, Language.get('wcf.global.form.error.empty')); + this.throwError(this._textarea, Language.get("wcf.global.form.error.empty")); return false; } - var data = { + const data = { api: this, editor: this._getEditor(), message: this._getEditor().code.get(), - valid: true + valid: true, }; - EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_text', data); - return (data.valid !== false); - }, + EventHandler.fire("com.woltlab.wcf.redactor2", "validate_text", data); + return data.valid; + } /** * 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) { - elInnerError(element, (message === 'empty' ? Language.get('wcf.global.form.error.empty') : message)); - }, + throwError(element, message) { + Util_1.default.innerError(element, message === "empty" ? Language.get("wcf.global.form.error.empty") : message); + } /** * Displays a loading spinner while the request is processed by the server. - * - * @protected */ - _showLoadingOverlay: function () { + _showLoadingOverlay() { if (this._loadingOverlay === null) { - this._loadingOverlay = elCreate('div'); - this._loadingOverlay.className = 'messageContentLoadingOverlay'; + this._loadingOverlay = document.createElement("div"); + this._loadingOverlay.className = "messageContentLoadingOverlay"; this._loadingOverlay.innerHTML = ''; } - this._content.classList.add('loading'); + 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); + _hideLoadingOverlay() { + this._content.classList.remove("loading"); + const loadingOverlay = this._content.querySelector(".messageContentLoadingOverlay"); if (loadingOverlay !== null) { - loadingOverlay.parentNode.removeChild(loadingOverlay); + loadingOverlay.remove(); } - }, + } /** * Resets the editor contents and notifies event listeners. - * - * @protected */ - _reset: function () { - this._getEditor().code.set('

\u200b

'); - EventHandler.fire('com.woltlab.wcf.redactor2', 'reset_text'); - }, + _reset() { + this._getEditor().code.set("

\u200b

"); + EventHandler.fire("com.woltlab.wcf.redactor2", "reset_text"); + } /** * Handles errors occurred during server processing. - * - * @param {Object} data response data - * @protected */ - _handleError: function (data) { - var parameters = { + _handleError(data) { + const parameters = { api: this, cancel: false, - returnValues: data.returnValues + returnValues: data.returnValues, }; - EventHandler.fire('com.woltlab.wcf.redactor2', 'handleError_text', parameters); - if (parameters.cancel !== true) { - //noinspection JSUnresolvedVariable + EventHandler.fire("com.woltlab.wcf.redactor2", "handleError_text", parameters); + if (!parameters.cancel) { this.throwError(this._textarea, data.returnValues.realErrorMessage); } - }, + } /** * Returns the current editor instance. - * - * @return {Object} editor instance - * @protected */ - _getEditor: function () { + _getEditor() { if (this._editor === null) { - if (typeof window.jQuery === 'function') { - this._editor = window.jQuery(this._textarea).data('redactor'); + 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) { + _insertMessage(data) { this._getEditor().WoltLabAutosave.reset(); // redirect to new page - //noinspection JSUnresolvedVariable if (data.returnValues.url) { - //noinspection JSUnresolvedVariable - if (window.location == data.returnValues.url) { + if (window.location.href == data.returnValues.url) { window.location.reload(); } - window.location = data.returnValues.url; + window.location.href = data.returnValues.url; } else { - //noinspection JSUnresolvedVariable if (data.returnValues.template) { - var elementId; + let elementId; // insert HTML - if (elData(this._container, 'sort-order') === 'DESC') { - //noinspection JSUnresolvedVariable - DomUtil.insertHtml(data.returnValues.template, this._container, 'after'); - elementId = DomUtil.identify(this._container.nextElementSibling); + if (this._container.dataset.sortOrder === "DESC") { + Util_1.default.insertHtml(data.returnValues.template, this._container, "after"); + elementId = Util_1.default.identify(this._container.nextElementSibling); } else { - var insertBefore = this._container; - if (insertBefore.previousElementSibling && insertBefore.previousElementSibling.classList.contains('messageListPagination')) { + let insertBefore = this._container; + if (insertBefore.previousElementSibling && + insertBefore.previousElementSibling.classList.contains("messageListPagination")) { insertBefore = insertBefore.previousElementSibling; } - //noinspection JSUnresolvedVariable - DomUtil.insertHtml(data.returnValues.template, insertBefore, 'before'); - elementId = DomUtil.identify(insertBefore.previousElementSibling); + Util_1.default.insertHtml(data.returnValues.template, insertBefore, "before"); + elementId = Util_1.default.identify(insertBefore.previousElementSibling); } // update last post time - //noinspection JSUnresolvedVariable - elData(this._container, 'last-post-time', data.returnValues.lastPostTime); - window.history.replaceState(undefined, '', '#' + elementId); - UiScroll.element(elById(elementId)); + this._container.dataset.lastPostTime = data.returnValues.lastPostTime.toString(); + window.history.replaceState(undefined, "", `#${elementId}`); + UiScroll.element(document.getElementById(elementId)); } UiNotification.show(Language.get(this._options.successMessage)); if (this._options.quoteManager) { this._options.quoteManager.countQuotes(); } - DomChangeListener.trigger(); + Listener_1.default.trigger(); } - }, + } /** * @param {{returnValues:{guestDialog:string,guestDialogID:string}}} data * @protected */ - _ajaxSuccess: function (data) { - if (!User.userId && !data.returnValues.guestDialogID) { + _ajaxSuccess(data) { + if (!User_1.default.userId && !data.returnValues.guestDialogID) { throw new Error("Missing 'guestDialogID' return value for guest."); } - if (!User.userId && data.returnValues.guestDialog) { - UiDialog.openStatic(data.returnValues.guestDialogID, data.returnValues.guestDialog, { + if (!User_1.default.userId && data.returnValues.guestDialog) { + const guestDialogId = data.returnValues.guestDialogID; + Dialog_1.default.openStatic(guestDialogId, data.returnValues.guestDialog, { closable: false, onClose: function () { - if (ControllerCaptcha.has(data.returnValues.guestDialogID)) { - ControllerCaptcha.delete(data.returnValues.guestDialogID); + if (Captcha_1.default.has(guestDialogId)) { + Captcha_1.default.delete(guestDialogId); } }, - title: Language.get('wcf.global.confirmation.title') + title: Language.get("wcf.global.confirmation.title"), }); - var dialog = UiDialog.getDialog(data.returnValues.guestDialogID); - elBySel('input[type=submit]', dialog.content).addEventListener('click', this._submitGuestDialog.bind(this)); - elBySel('input[type=text]', dialog.content).addEventListener('keypress', this._submitGuestDialog.bind(this)); - this._guestDialogId = data.returnValues.guestDialogID; + const dialog = Dialog_1.default.getDialog(guestDialogId); + const submit = dialog.content.querySelector("input[type=submit]"); + submit.addEventListener("click", (ev) => this._submitGuestDialog(ev)); + const input = dialog.content.querySelector("input[type=text]"); + input.addEventListener("keypress", (ev) => this._submitGuestDialog(ev)); + this._guestDialogId = guestDialogId; } else { this._insertMessage(data); - if (!User.userId) { - UiDialog.close(data.returnValues.guestDialogID); + if (!User_1.default.userId) { + Dialog_1.default.close(data.returnValues.guestDialogID); } this._reset(); this._hideLoadingOverlay(); } - }, - _ajaxFailure: function (data) { + } + _ajaxFailure(data) { this._hideLoadingOverlay(); - //noinspection JSUnresolvedVariable if (data === null || data.returnValues === undefined || data.returnValues.realErrorMessage === undefined) { return true; } this._handleError(data); return false; - }, - _ajaxSetup: function () { + } + _ajaxSetup() { return { data: { - actionName: 'quickReply', + actionName: "quickReply", className: this._options.ajax.className, - interfaceName: 'wcf\\data\\IMessageQuickReplyAction' + interfaceName: "wcf\\data\\IMessageQuickReplyAction", }, - silent: true + silent: true, }; } - }; + } + Core.enableLegacyInheritance(UiMessageReply); return UiMessageReply; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Reply.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Reply.js deleted file mode 100644 index 19e3f37f75..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Reply.js +++ /dev/null @@ -1,434 +0,0 @@ -/** - * Handles user interaction with the quick reply feature. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Ui/Message/Reply - */ -define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Dom/Traverse', 'Ui/Dialog', 'Ui/Notification', 'WoltLabSuite/Core/Ui/Scroll', 'EventKey', 'User', 'WoltLabSuite/Core/Controller/Captcha'], - function(Ajax, Core, EventHandler, Language, DomChangeListener, DomUtil, DomTraverse, UiDialog, UiNotification, UiScroll, EventKey, User, ControllerCaptcha) { - "use strict"; - - if (!COMPILER_TARGET_DEFAULT) { - var Fake = function() {}; - Fake.prototype = { - init: function() {}, - _submitGuestDialog: function() {}, - _submit: function() {}, - _validate: function() {}, - throwError: function() {}, - _showLoadingOverlay: function() {}, - _hideLoadingOverlay: function() {}, - _reset: function() {}, - _handleError: function() {}, - _getEditor: function() {}, - _insertMessage: function() {}, - _ajaxSuccess: function() {}, - _ajaxFailure: function() {}, - _ajaxSetup: function() {} - }; - return Fake; - } - - /** - * @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: '' - }, - quoteManager: null, - successMessage: 'wcf.global.success.add' - }, options); - - this._container = elById('messageQuickReply'); - this._content = elBySel('.messageContent', this._container); - this._textarea = elById('text'); - this._editor = null; - this._guestDialogId = ''; - 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"]', this._container); - submitButton.addEventListener('click', submitCallback); - - // bind reply button - var replyButtons = elBySelAll('.jsQuickReply'); - for (var i = 0, length = replyButtons.length; i < length; i++) { - replyButtons[i].addEventListener('click', (function(event) { - event.preventDefault(); - - this._getEditor().WoltLabReply.showEditor(); - - UiScroll.element(this._container, (function() { - this._getEditor().WoltLabCaret.endOfEditor(); - }).bind(this)); - }).bind(this)); - } - }, - - /** - * Submits the guest dialog. - * - * @param {Event} event - * @protected - */ - _submitGuestDialog: function(event) { - // only submit when enter key is pressed - if (event.type === 'keypress' && !EventKey.Enter(event)) { - return; - } - - var usernameInput = elBySel('input[name=username]', event.currentTarget.closest('.dialogContent')); - if (usernameInput.value === '') { - elInnerError(usernameInput, Language.get('wcf.global.form.error.empty')); - usernameInput.closest('dl').classList.add('formError'); - - return; - } - - var parameters = { - parameters: { - data: { - username: usernameInput.value - } - } - }; - - //noinspection JSCheckFunctionSignatures - var captchaId = elData(event.currentTarget, 'captcha-id'); - if (ControllerCaptcha.has(captchaId)) { - var data = ControllerCaptcha.getData(captchaId); - if (data instanceof Promise) { - data.then((function (data) { - parameters = Core.extend(parameters, data); - this._submit(undefined, parameters); - }).bind(this)); - } - else { - parameters = Core.extend(parameters, ControllerCaptcha.getData(captchaId)); - this._submit(undefined, parameters); - } - } - else { - this._submit(undefined, parameters); - } - }, - - /** - * Validates the message and submits it to the server. - * - * @param {Event?} event event object - * @param {Object?} additionalParameters additional parameters sent to the server - * @protected - */ - _submit: function(event, additionalParameters) { - if (event) { - event.preventDefault(); - } - - // Ignore requests to submit the message while a previous request is still pending. - if (this._content.classList.contains('loading')) { - if (!this._guestDialogId || !UiDialog.isOpen(this._guestDialogId)) { - return; - } - } - - 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() }; - parameters.removeQuoteIDs = (this._options.quoteManager) ? this._options.quoteManager.getQuotesMarkedForRemoval() : []; - - // add any available settings - var settingsContainer = elById('settings_text'); - if (settingsContainer) { - elBySelAll('input, select, textarea', settingsContainer, function (element) { - if (element.nodeName === 'INPUT' && (element.type === 'checkbox' || element.type === 'radio')) { - if (!element.checked) { - return; - } - } - - var name = element.name; - if (parameters.hasOwnProperty(name)) { - throw new Error("Variable overshadowing, key '" + name + "' is already present."); - } - - parameters[name] = element.value.trim(); - }); - } - - EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_text', parameters.data); - - if (!User.userId && !additionalParameters) { - parameters.requireGuestDialog = true; - } - - Ajax.api(this, Core.extend({ - parameters: parameters - }, additionalParameters)); - }, - - /** - * Validates the message and invokes listeners to perform additional validation. - * - * @return {boolean} validation result - * @protected - */ - _validate: function() { - // remove all existing error elements - elBySelAll('.innerError', this._container, elRemove); - - // 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) { - elInnerError(element, (message === 'empty' ? Language.get('wcf.global.form.error.empty') : message)); - }, - - /** - * 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 occurred during server processing. - * - * @param {Object} data response data - * @protected - */ - _handleError: function(data) { - var parameters = { - api: this, - cancel: false, - returnValues: data.returnValues - }; - EventHandler.fire('com.woltlab.wcf.redactor2', 'handleError_text', parameters); - - if (parameters.cancel !== true) { - //noinspection JSUnresolvedVariable - this.throwError(this._textarea, data.returnValues.realErrorMessage); - } - }, - - /** - * 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) { - this._getEditor().WoltLabAutosave.reset(); - - // redirect to new page - //noinspection JSUnresolvedVariable - if (data.returnValues.url) { - //noinspection JSUnresolvedVariable - if (window.location == data.returnValues.url) { - window.location.reload(); - } - window.location = data.returnValues.url; - } - else { - //noinspection JSUnresolvedVariable - if (data.returnValues.template) { - var elementId; - - // insert HTML - if (elData(this._container, 'sort-order') === 'DESC') { - //noinspection JSUnresolvedVariable - DomUtil.insertHtml(data.returnValues.template, this._container, 'after'); - elementId = DomUtil.identify(this._container.nextElementSibling); - } - else { - var insertBefore = this._container; - if (insertBefore.previousElementSibling && insertBefore.previousElementSibling.classList.contains('messageListPagination')) { - insertBefore = insertBefore.previousElementSibling; - } - - //noinspection JSUnresolvedVariable - DomUtil.insertHtml(data.returnValues.template, insertBefore, 'before'); - elementId = DomUtil.identify(insertBefore.previousElementSibling); - } - - // update last post time - //noinspection JSUnresolvedVariable - elData(this._container, 'last-post-time', data.returnValues.lastPostTime); - - window.history.replaceState(undefined, '', '#' + elementId); - UiScroll.element(elById(elementId)); - } - - UiNotification.show(Language.get(this._options.successMessage)); - - if (this._options.quoteManager) { - this._options.quoteManager.countQuotes(); - } - - DomChangeListener.trigger(); - } - }, - - /** - * @param {{returnValues:{guestDialog:string,guestDialogID:string}}} data - * @protected - */ - _ajaxSuccess: function(data) { - if (!User.userId && !data.returnValues.guestDialogID) { - throw new Error("Missing 'guestDialogID' return value for guest."); - } - - if (!User.userId && data.returnValues.guestDialog) { - UiDialog.openStatic(data.returnValues.guestDialogID, data.returnValues.guestDialog, { - closable: false, - onClose: function() { - if (ControllerCaptcha.has(data.returnValues.guestDialogID)) { - ControllerCaptcha.delete(data.returnValues.guestDialogID); - } - }, - title: Language.get('wcf.global.confirmation.title') - }); - - var dialog = UiDialog.getDialog(data.returnValues.guestDialogID); - elBySel('input[type=submit]', dialog.content).addEventListener('click', this._submitGuestDialog.bind(this)); - elBySel('input[type=text]', dialog.content).addEventListener('keypress', this._submitGuestDialog.bind(this)); - - this._guestDialogId = data.returnValues.guestDialogID; - } - else { - this._insertMessage(data); - - if (!User.userId) { - UiDialog.close(data.returnValues.guestDialogID); - } - - this._reset(); - - this._hideLoadingOverlay(); - } - }, - - _ajaxFailure: function(data) { - this._hideLoadingOverlay(); - - //noinspection JSUnresolvedVariable - if (data === null || data.returnValues === undefined || data.returnValues.realErrorMessage === undefined) { - return true; - } - - this._handleError(data); - - return false; - }, - - _ajaxSetup: function() { - return { - data: { - actionName: 'quickReply', - className: this._options.ajax.className, - interfaceName: 'wcf\\data\\IMessageQuickReplyAction' - }, - silent: true - }; - } - }; - - return UiMessageReply; -}); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Reply.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Reply.ts new file mode 100644 index 0000000000..8a8a071e16 --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/Reply.ts @@ -0,0 +1,429 @@ +/** + * Handles user interaction with the quick reply feature. + * + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/Message/Reply + */ + +import * as Ajax from "../../Ajax"; +import { AjaxCallbackSetup, ResponseData } from "../../Ajax/Data"; +import * as Core from "../../Core"; +import * as EventHandler from "../../Event/Handler"; +import * as Language from "../../Language"; +import DomChangeListener from "../../Dom/Change/Listener"; +import DomUtil from "../../Dom/Util"; +import UiDialog from "../Dialog"; +import * as UiNotification from "../Notification"; +import User from "../../User"; +import ControllerCaptcha from "../../Controller/Captcha"; +import { RedactorEditor } from "../Redactor/Editor"; +import * as UiScroll from "../Scroll"; + +interface MessageReplyOptions { + ajax: { + className: string; + }; + quoteManager: any; + successMessage: string; +} + +interface AjaxResponse { + returnValues: { + guestDialog?: string; + guestDialogID?: string; + lastPostTime: number; + template?: string; + url?: string; + }; +} + +class UiMessageReply { + protected readonly _container: HTMLElement; + protected readonly _content: HTMLElement; + protected _editor: RedactorEditor | null = null; + protected _guestDialogId = ""; + protected _loadingOverlay: HTMLElement | null = null; + protected readonly _options: MessageReplyOptions; + protected readonly _textarea: HTMLTextAreaElement; + + /** + * Initializes a new quick reply field. + */ + constructor(opts: Partial) { + this._options = Core.extend( + { + ajax: { + className: "", + }, + quoteManager: null, + successMessage: "wcf.global.success.add", + }, + opts, + ) as MessageReplyOptions; + + this._container = document.getElementById("messageQuickReply") as HTMLElement; + this._content = this._container.querySelector(".messageContent") as HTMLElement; + this._textarea = document.getElementById("text") as HTMLTextAreaElement; + + // prevent marking of text for quoting + this._container.querySelector(".message")!.classList.add("jsInvalidQuoteTarget"); + + // handle submit button + const submitButton = this._container.querySelector('button[data-type="save"]') as HTMLButtonElement; + submitButton.addEventListener("click", (ev) => this._submit(ev)); + + // bind reply button + document.querySelectorAll(".jsQuickReply").forEach((replyButton: HTMLAnchorElement) => { + replyButton.addEventListener("click", (event) => { + event.preventDefault(); + + this._getEditor().WoltLabReply.showEditor(); + + UiScroll.element(this._container, () => { + this._getEditor().WoltLabCaret.endOfEditor(); + }); + }); + }); + } + + /** + * Submits the guest dialog. + */ + protected _submitGuestDialog(event: KeyboardEvent | MouseEvent): void { + // only submit when enter key is pressed + if (event instanceof KeyboardEvent && event.key !== "Enter") { + return; + } + + const target = event.currentTarget as HTMLElement; + const dialogContent = target.closest(".dialogContent")!; + const usernameInput = dialogContent.querySelector("input[name=username]") as HTMLInputElement; + if (usernameInput.value === "") { + DomUtil.innerError(usernameInput, Language.get("wcf.global.form.error.empty")); + usernameInput.closest("dl")!.classList.add("formError"); + + return; + } + + let parameters: ArbitraryObject = { + parameters: { + data: { + username: usernameInput.value, + }, + }, + }; + + const captchaId = target.dataset.captchaId!; + if (ControllerCaptcha.has(captchaId)) { + const data = ControllerCaptcha.getData(captchaId); + if (data instanceof Promise) { + void data.then((data) => { + parameters = Core.extend(parameters, data) as ArbitraryObject; + this._submit(undefined, parameters); + }); + } else { + parameters = Core.extend( + parameters, + ControllerCaptcha.getData(captchaId) as ArbitraryObject, + ) as ArbitraryObject; + this._submit(undefined, parameters); + } + } else { + this._submit(undefined, parameters); + } + } + + /** + * Validates the message and submits it to the server. + */ + protected _submit(event: MouseEvent | undefined, additionalParameters?: ArbitraryObject): void { + if (event) { + event.preventDefault(); + } + + // Ignore requests to submit the message while a previous request is still pending. + if (this._content.classList.contains("loading")) { + if (!this._guestDialogId || !UiDialog.isOpen(this._guestDialogId)) { + return; + } + } + + if (!this._validate()) { + // validation failed, bail out + return; + } + + this._showLoadingOverlay(); + + // build parameters + const parameters: ArbitraryObject = {}; + Object.entries(this._container.dataset).forEach(([key, value]) => { + parameters[key.replace(/Id$/, "ID")] = value; + }); + + parameters.data = { message: this._getEditor().code.get() }; + parameters.removeQuoteIDs = this._options.quoteManager + ? this._options.quoteManager.getQuotesMarkedForRemoval() + : []; + + // add any available settings + const settingsContainer = document.getElementById("settings_text"); + if (settingsContainer) { + settingsContainer + .querySelectorAll("input, select, textarea") + .forEach((element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement) => { + if (element.nodeName === "INPUT" && (element.type === "checkbox" || element.type === "radio")) { + if (!(element as HTMLInputElement).checked) { + return; + } + } + + const name = element.name; + if (Object.prototype.hasOwnProperty.call(parameters, name)) { + throw new Error(`Variable overshadowing, key '${name}' is already present.`); + } + + parameters[name] = element.value.trim(); + }); + } + + EventHandler.fire("com.woltlab.wcf.redactor2", "submit_text", parameters.data as any); + + if (!User.userId && !additionalParameters) { + parameters.requireGuestDialog = true; + } + + Ajax.api( + this, + Core.extend( + { + parameters: parameters, + }, + additionalParameters as any, + ), + ); + } + + /** + * Validates the message and invokes listeners to perform additional validation. + */ + protected _validate(): boolean { + // remove all existing error elements + this._container.querySelectorAll(".innerError").forEach((el) => el.remove()); + + // check if editor contains actual content + if (this._getEditor().utils.isEmpty()) { + this.throwError(this._textarea, Language.get("wcf.global.form.error.empty")); + return false; + } + + const 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; + } + + /** + * Throws an error by adding an inline error to target element. + * + * @param {Element} element erroneous element + * @param {string} message error message + */ + throwError(element: HTMLElement, message: string): void { + DomUtil.innerError(element, message === "empty" ? Language.get("wcf.global.form.error.empty") : message); + } + + /** + * Displays a loading spinner while the request is processed by the server. + */ + protected _showLoadingOverlay(): void { + if (this._loadingOverlay === null) { + this._loadingOverlay = document.createElement("div"); + this._loadingOverlay.className = "messageContentLoadingOverlay"; + this._loadingOverlay.innerHTML = ''; + } + + this._content.classList.add("loading"); + this._content.appendChild(this._loadingOverlay); + } + + /** + * Hides the loading spinner. + */ + protected _hideLoadingOverlay(): void { + this._content.classList.remove("loading"); + + const loadingOverlay = this._content.querySelector(".messageContentLoadingOverlay"); + if (loadingOverlay !== null) { + loadingOverlay.remove(); + } + } + + /** + * Resets the editor contents and notifies event listeners. + */ + protected _reset(): void { + this._getEditor().code.set("

\u200b

"); + + EventHandler.fire("com.woltlab.wcf.redactor2", "reset_text"); + } + + /** + * Handles errors occurred during server processing. + */ + protected _handleError(data: ResponseData): void { + const parameters = { + api: this, + cancel: false, + returnValues: data.returnValues, + }; + EventHandler.fire("com.woltlab.wcf.redactor2", "handleError_text", parameters); + + if (!parameters.cancel) { + this.throwError(this._textarea, data.returnValues.realErrorMessage); + } + } + + /** + * Returns the current editor instance. + */ + protected _getEditor(): RedactorEditor { + if (this._editor === null) { + if (typeof window.jQuery === "function") { + this._editor = window.jQuery(this._textarea).data("redactor") as RedactorEditor; + } 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. + */ + protected _insertMessage(data: AjaxResponse): void { + this._getEditor().WoltLabAutosave.reset(); + + // redirect to new page + if (data.returnValues.url) { + if (window.location.href == data.returnValues.url) { + window.location.reload(); + } + window.location.href = data.returnValues.url; + } else { + if (data.returnValues.template) { + let elementId: string; + + // insert HTML + if (this._container.dataset.sortOrder === "DESC") { + DomUtil.insertHtml(data.returnValues.template, this._container, "after"); + elementId = DomUtil.identify(this._container.nextElementSibling!); + } else { + let insertBefore = this._container; + if ( + insertBefore.previousElementSibling && + insertBefore.previousElementSibling.classList.contains("messageListPagination") + ) { + insertBefore = insertBefore.previousElementSibling as HTMLElement; + } + + DomUtil.insertHtml(data.returnValues.template, insertBefore, "before"); + elementId = DomUtil.identify(insertBefore.previousElementSibling!); + } + + // update last post time + this._container.dataset.lastPostTime = data.returnValues.lastPostTime.toString(); + + window.history.replaceState(undefined, "", `#${elementId}`); + UiScroll.element(document.getElementById(elementId)!); + } + + UiNotification.show(Language.get(this._options.successMessage)); + + if (this._options.quoteManager) { + this._options.quoteManager.countQuotes(); + } + + DomChangeListener.trigger(); + } + } + + /** + * @param {{returnValues:{guestDialog:string,guestDialogID:string}}} data + * @protected + */ + _ajaxSuccess(data: AjaxResponse): void { + if (!User.userId && !data.returnValues.guestDialogID) { + throw new Error("Missing 'guestDialogID' return value for guest."); + } + + if (!User.userId && data.returnValues.guestDialog) { + const guestDialogId = data.returnValues.guestDialogID!; + + UiDialog.openStatic(guestDialogId, data.returnValues.guestDialog, { + closable: false, + onClose: function () { + if (ControllerCaptcha.has(guestDialogId)) { + ControllerCaptcha.delete(guestDialogId); + } + }, + title: Language.get("wcf.global.confirmation.title"), + }); + + const dialog = UiDialog.getDialog(guestDialogId)!; + const submit = dialog.content.querySelector("input[type=submit]") as HTMLInputElement; + submit.addEventListener("click", (ev) => this._submitGuestDialog(ev)); + const input = dialog.content.querySelector("input[type=text]") as HTMLInputElement; + input.addEventListener("keypress", (ev) => this._submitGuestDialog(ev)); + + this._guestDialogId = guestDialogId; + } else { + this._insertMessage(data); + + if (!User.userId) { + UiDialog.close(data.returnValues.guestDialogID!); + } + + this._reset(); + + this._hideLoadingOverlay(); + } + } + + _ajaxFailure(data: ResponseData): boolean { + this._hideLoadingOverlay(); + + if (data === null || data.returnValues === undefined || data.returnValues.realErrorMessage === undefined) { + return true; + } + + this._handleError(data); + + return false; + } + + _ajaxSetup(): ReturnType { + return { + data: { + actionName: "quickReply", + className: this._options.ajax.className, + interfaceName: "wcf\\data\\IMessageQuickReplyAction", + }, + silent: true, + }; + } +} + +Core.enableLegacyInheritance(UiMessageReply); + +export = UiMessageReply; diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Editor.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Editor.ts index 714bd4e386..2882d5fc98 100644 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Editor.ts +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Redactor/Editor.ts @@ -44,15 +44,22 @@ export interface RedactorEditor { save(): void; }; utils: { - isEmpty(html: string): boolean; + isEmpty(html?: string): boolean; }; + WoltLabAutosave: { + reset(): void; + }; WoltLabCaret: { + endOfEditor(): void; paragraphAfterBlock(quote: HTMLElement): void; }; WoltLabEvent: { register(event: string, callback: (data: WoltLabEventData) => void): void; }; + WoltLabReply: { + showEditor(): void; + }; WoltLabSource: { isActive(): boolean; }; -- 2.20.1