From 89d5f9df0a0fc62605b7429fe5f772634ce09477 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 5 Nov 2020 16:09:06 +0100 Subject: [PATCH] Convert `Ui/Message/InlineEditor` to TypeScript --- global.d.ts | 2 + .../Core/Ui/Message/InlineEditor.js | 702 +++++++--------- .../Core/Ui/Message/InlineEditor.js | 757 ------------------ .../Core/Ui/Message/InlineEditor.ts | 723 +++++++++++++++++ 4 files changed, 1033 insertions(+), 1151 deletions(-) delete mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.js create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.ts diff --git a/global.d.ts b/global.d.ts index 23265d7316..466c32a2ae 100644 --- a/global.d.ts +++ b/global.d.ts @@ -29,4 +29,6 @@ declare global { interface String { hashCode: () => string; } + + type ArbitraryObject = Record; } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/InlineEditor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/InlineEditor.js index 1fc2489ce3..4617cf60ed 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/InlineEditor.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/InlineEditor.js @@ -1,419 +1,363 @@ /** * Flexible message inline editor. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Ui/Message/InlineEditor + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/Message/InlineEditor */ -define([ - 'Ajax', 'Core', 'Dictionary', 'Environment', - 'EventHandler', 'Language', 'ObjectMap', 'Dom/ChangeListener', 'Dom/Traverse', - 'Dom/Util', 'Ui/Notification', 'Ui/ReusableDropdown', 'WoltLabSuite/Core/Ui/Scroll' -], function (Ajax, Core, Dictionary, Environment, EventHandler, Language, ObjectMap, DomChangeListener, DomTraverse, DomUtil, UiNotification, UiReusableDropdown, UiScroll) { +define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Dom/Change/Listener", "../../Dom/Util", "../../Environment", "../../Event/Handler", "../../Language", "../Dropdown/Reusable", "../Notification", "../Scroll"], function (require, exports, tslib_1, Ajax, Core, Listener_1, Util_1, Environment, EventHandler, Language, UiDropdownReusable, UiNotification, UiScroll) { "use strict"; - if (!COMPILER_TARGET_DEFAULT) { - var Fake = function () { }; - Fake.prototype = { - init: function () { }, - rebuild: function () { }, - _click: function () { }, - _clickDropdown: function () { }, - _dropdownBuild: function () { }, - _dropdownToggle: function () { }, - _dropdownGetItems: function () { }, - _dropdownOpen: function () { }, - _dropdownSelect: function () { }, - _clickDropdownItem: function () { }, - _prepare: function () { }, - _showEditor: function () { }, - _restoreMessage: function () { }, - _save: function () { }, - _validate: function () { }, - throwError: function () { }, - _showMessage: function () { }, - _hideEditor: function () { }, - _restoreEditor: function () { }, - _destroyEditor: function () { }, - _getHash: function () { }, - _updateHistory: function () { }, - _getEditorId: function () { }, - _getObjectId: function () { }, - _ajaxFailure: function () { }, - _ajaxSuccess: function () { }, - _ajaxSetup: function () { }, - legacyEdit: function () { } - }; - return Fake; - } - /** - * @constructor - */ - function UiMessageInlineEditor(options) { this.init(options); } - UiMessageInlineEditor.prototype = { + Ajax = tslib_1.__importStar(Ajax); + Core = tslib_1.__importStar(Core); + Listener_1 = tslib_1.__importDefault(Listener_1); + Util_1 = tslib_1.__importDefault(Util_1); + Environment = tslib_1.__importStar(Environment); + EventHandler = tslib_1.__importStar(EventHandler); + Language = tslib_1.__importStar(Language); + UiDropdownReusable = tslib_1.__importStar(UiDropdownReusable); + UiNotification = tslib_1.__importStar(UiNotification); + UiScroll = tslib_1.__importStar(UiScroll); + class UiMessageInlineEditor { /** * Initializes the message inline editor. - * - * @param {Object} options list of configuration options */ - init: function (options) { + constructor(opts) { this._activeDropdownElement = null; this._activeElement = null; this._dropdownMenu = null; - this._elements = new ObjectMap(); + this._elements = new WeakMap(); this._options = Core.extend({ canEditInline: false, - className: '', + className: "", containerId: 0, - dropdownIdentifier: '', - editorPrefix: 'messageEditor', - messageSelector: '.jsMessage', - quoteManager: null - }, options); + dropdownIdentifier: "", + editorPrefix: "messageEditor", + messageSelector: ".jsMessage", + quoteManager: null, + }, opts); this.rebuild(); - DomChangeListener.add('Ui/Message/InlineEdit_' + this._options.className, this.rebuild.bind(this)); - }, + Listener_1.default.add(`Ui/Message/InlineEdit_${this._options.className}`, () => this.rebuild()); + } /** * Initializes each applicable message, should be called whenever new * messages are being displayed. */ - rebuild: function () { - var button, canEdit, element, elements = elBySelAll(this._options.messageSelector); - for (var i = 0, length = elements.length; i < length; i++) { - element = elements[i]; + rebuild() { + document.querySelectorAll(this._options.messageSelector).forEach((element) => { if (this._elements.has(element)) { - continue; + return; } - button = elBySel('.jsMessageEditButton', element); + const button = element.querySelector(".jsMessageEditButton"); if (button !== null) { - canEdit = elDataBool(element, 'can-edit'); - if (this._options.canEditInline || elDataBool(element, 'can-edit-inline')) { - button.addEventListener('click', this._clickDropdown.bind(this, element)); - button.classList.add('jsDropdownEnabled'); + const canEdit = Core.stringToBool(element.dataset.canEdit || ""); + const canEditInline = Core.stringToBool(element.dataset.canEditInline || ""); + if (this._options.canEditInline || canEditInline) { + button.addEventListener("click", (ev) => this._clickDropdown(element, ev)); + button.classList.add("jsDropdownEnabled"); if (canEdit) { - button.addEventListener('dblclick', this._click.bind(this, element)); + button.addEventListener("dblclick", (ev) => this._click(element, ev)); } } else if (canEdit) { - button.addEventListener('click', this._click.bind(this, element)); + button.addEventListener("click", (ev) => this._click(element, ev)); } } - var messageBody = elBySel('.messageBody', element); - var messageFooter = elBySel('.messageFooter', element); - var messageHeader = elBySel('.messageHeader', element); + const messageBody = element.querySelector(".messageBody"); + const messageFooter = element.querySelector(".messageFooter"); + const messageFooterButtons = messageFooter.querySelector(".messageFooterButtons"); + const messageHeader = element.querySelector(".messageHeader"); + const messageText = messageBody.querySelector(".messageText"); this._elements.set(element, { - button: button, - messageBody: messageBody, + button, + messageBody, messageBodyEditor: null, - messageFooter: messageFooter, - messageFooterButtons: elBySel('.messageFooterButtons', messageFooter), - messageHeader: messageHeader, - messageText: elBySel('.messageText', messageBody) + messageFooter, + messageFooterButtons, + messageHeader, + messageText, }); - } - }, + }); + } /** * Handles clicks on the edit button or the edit dropdown item. - * - * @param {Element} element message element - * @param {?Event} event event object - * @protected */ - _click: function (element, event) { - if (element === null) + _click(element, event) { + if (element === null) { element = this._activeDropdownElement; - if (event) + } + if (event) { event.preventDefault(); + } if (this._activeElement === null) { this._activeElement = element; this._prepare(); Ajax.api(this, { - actionName: 'beginEdit', + actionName: "beginEdit", parameters: { containerID: this._options.containerId, - objectID: this._getObjectId(element) - } + objectID: this._getObjectId(element), + }, }); } else { - UiNotification.show('wcf.message.error.editorAlreadyInUse', null, 'warning'); + UiNotification.show("wcf.message.error.editorAlreadyInUse", undefined, "warning"); } - }, + } /** * Creates and opens the dropdown on first usage. - * - * @param {Element} element message element - * @param {Object} event event object - * @protected */ - _clickDropdown: function (element, event) { + _clickDropdown(element, event) { event.preventDefault(); - var button = event.currentTarget; - if (button.classList.contains('dropdownToggle')) { + const button = event.currentTarget; + if (button.classList.contains("dropdownToggle")) { return; } - button.classList.add('dropdownToggle'); - button.parentNode.classList.add('dropdown'); - (function (button, element) { - button.addEventListener('click', (function (event) { - event.preventDefault(); - event.stopPropagation(); - this._activeDropdownElement = element; - UiReusableDropdown.toggleDropdown(this._options.dropdownIdentifier, button); - }).bind(this)); - }).bind(this)(button, element); + button.classList.add("dropdownToggle"); + button.parentElement.classList.add("dropdown"); + button.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + this._activeDropdownElement = element; + UiDropdownReusable.toggleDropdown(this._options.dropdownIdentifier, button); + }); // build dropdown if (this._dropdownMenu === null) { - this._dropdownMenu = elCreate('ul'); - this._dropdownMenu.className = 'dropdownMenu'; - var items = this._dropdownGetItems(); - EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownInit_' + this._options.dropdownIdentifier, { - items: items + this._dropdownMenu = document.createElement("ul"); + this._dropdownMenu.className = "dropdownMenu"; + const items = this._dropdownGetItems(); + EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownInit_${this._options.dropdownIdentifier}`, { + items: items, }); this._dropdownBuild(items); - UiReusableDropdown.init(this._options.dropdownIdentifier, this._dropdownMenu); - UiReusableDropdown.registerCallback(this._options.dropdownIdentifier, this._dropdownToggle.bind(this)); + UiDropdownReusable.init(this._options.dropdownIdentifier, this._dropdownMenu); + UiDropdownReusable.registerCallback(this._options.dropdownIdentifier, (containerId, action) => this._dropdownToggle(containerId, action)); } - setTimeout(function () { - Core.triggerEvent(button, 'click'); - }, 10); - }, + setTimeout(() => button.click(), 10); + } /** * Creates the dropdown menu on first usage. - * - * @param {Object} items list of dropdown items - * @protected */ - _dropdownBuild: function (items) { - var item, label, listItem; - var callbackClick = this._clickDropdownItem.bind(this); - for (var i = 0, length = items.length; i < length; i++) { - item = items[i]; - listItem = elCreate('li'); - elData(listItem, 'item', item.item); - if (item.item === 'divider') { - listItem.className = 'dropdownDivider'; + _dropdownBuild(items) { + items.forEach((item) => { + const listItem = document.createElement("li"); + listItem.dataset.item = item.item; + if (item.item === "divider") { + listItem.className = "dropdownDivider"; } else { - label = elCreate('span'); + const label = document.createElement("span"); label.textContent = Language.get(item.label); listItem.appendChild(label); - if (item.item === 'editItem') { - listItem.addEventListener('click', this._click.bind(this, null)); + if (item.item === "editItem") { + listItem.addEventListener("click", (ev) => this._click(null, ev)); } else { - listItem.addEventListener('click', callbackClick); + listItem.addEventListener("click", (ev) => this._clickDropdownItem(ev)); } } this._dropdownMenu.appendChild(listItem); - } - }, + }); + } /** * Callback for dropdown toggle. - * - * @param {int} containerId container id - * @param {string} action toggle action, either 'open' or 'close' - * @protected */ - _dropdownToggle: function (containerId, action) { - var elementData = this._elements.get(this._activeDropdownElement); - elementData.button.parentNode.classList[(action === 'open' ? 'add' : 'remove')]('dropdownOpen'); - elementData.messageFooterButtons.classList[(action === 'open' ? 'add' : 'remove')]('forceVisible'); - if (action === 'open') { - var visibility = this._dropdownOpen(); - EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownOpen_' + this._options.dropdownIdentifier, { - element: this._activeDropdownElement, - visibility: visibility - }); - var item, listItem, visiblePredecessor = false; - for (var i = 0; i < this._dropdownMenu.childElementCount; i++) { - listItem = this._dropdownMenu.children[i]; - item = elData(listItem, 'item'); - if (item === 'divider') { - if (visiblePredecessor) { - elShow(listItem); - visiblePredecessor = false; - } - else { - elHide(listItem); - } + _dropdownToggle(containerId, action) { + const elementData = this._elements.get(this._activeDropdownElement); + const buttonParent = elementData.button.parentElement; + if (action === "close") { + buttonParent.classList.remove("dropdownOpen"); + elementData.messageFooterButtons.classList.remove("forceVisible"); + return; + } + buttonParent.classList.add("dropdownOpen"); + elementData.messageFooterButtons.classList.add("forceVisible"); + const visibility = new Map(Object.entries(this._dropdownOpen())); + EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownOpen_${this._options.dropdownIdentifier}`, { + element: this._activeDropdownElement, + visibility, + }); + const dropdownMenu = this._dropdownMenu; + let visiblePredecessor = false; + const children = Array.from(dropdownMenu.children); + children.forEach((listItem, index) => { + const item = listItem.dataset.item; + if (item === "divider") { + if (visiblePredecessor) { + Util_1.default.show(listItem); + visiblePredecessor = false; } else { - if (objOwns(visibility, item) && visibility[item] === false) { - elHide(listItem); - // check if previous item was a divider - if (i > 0 && i + 1 === this._dropdownMenu.childElementCount) { - if (elData(listItem.previousElementSibling, 'item') === 'divider') { - elHide(listItem.previousElementSibling); - } + Util_1.default.hide(listItem); + } + } + else { + if (visibility.get(item) === false) { + Util_1.default.hide(listItem); + // check if previous item was a divider + if (index > 0 && index + 1 === children.length) { + const previousElementSibling = listItem.previousElementSibling; + if (previousElementSibling.dataset.item === "divider") { + Util_1.default.hide(previousElementSibling); } } - else { - elShow(listItem); - visiblePredecessor = true; - } + } + else { + Util_1.default.show(listItem); + visiblePredecessor = true; } } - } - }, + }); + } /** * Returns the list of dropdown items for this type. - * - * @return {Array} list of objects containing the type name and label - * @protected */ - _dropdownGetItems: function () { }, + _dropdownGetItems() { + // This should be an abstract method, but cannot be marked as such for backwards compatibility. + return []; + } /** * Invoked once the dropdown for this type is shown, expects a list of type name and a boolean value * to represent the visibility of each item. Items that do not appear in this list will be considered * visible. - * - * @return {Object} - * @protected */ - _dropdownOpen: function () { }, + _dropdownOpen() { + // This should be an abstract method, but cannot be marked as such for backwards compatibility. + return {}; + } /** * Invoked whenever the user selects an item from the dropdown menu, the selected item is passed as argument. - * - * @param {string} item selected dropdown item - * @protected */ - _dropdownSelect: function (item) { }, + _dropdownSelect(_item) { + // This should be an abstract method, but cannot be marked as such for backwards compatibility. + } /** * Handles clicks on a dropdown item. - * - * @param {Event} event event object - * @protected */ - _clickDropdownItem: function (event) { + _clickDropdownItem(event) { event.preventDefault(); - //noinspection JSCheckFunctionSignatures - var item = elData(event.currentTarget, 'item'); - var data = { + const target = event.currentTarget; + const item = target.dataset.item; + const data = { cancel: false, element: this._activeDropdownElement, - item: item + item, }; - EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownItemClick_' + this._options.dropdownIdentifier, data); - if (data.cancel === true) { + EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownItemClick_${this._options.dropdownIdentifier}`, data); + if (data.cancel) { event.preventDefault(); } else { this._dropdownSelect(item); } - }, + } /** * Prepares the message for editor display. - * - * @protected */ - _prepare: function () { - var data = this._elements.get(this._activeElement); - var messageBodyEditor = elCreate('div'); - messageBodyEditor.className = 'messageBody editor'; + _prepare() { + const data = this._elements.get(this._activeElement); + const messageBodyEditor = document.createElement("div"); + messageBodyEditor.className = "messageBody editor"; data.messageBodyEditor = messageBodyEditor; - var icon = elCreate('span'); - icon.className = 'icon icon48 fa-spinner'; + const icon = document.createElement("span"); + icon.className = "icon icon48 fa-spinner"; messageBodyEditor.appendChild(icon); - DomUtil.insertAfter(messageBodyEditor, data.messageBody); - elHide(data.messageBody); - }, + data.messageBody.insertAdjacentElement("afterend", messageBodyEditor); + Util_1.default.hide(data.messageBody); + } /** * Shows the message editor. - * - * @param {Object} data ajax response data - * @protected */ - _showEditor: function (data) { - var id = this._getEditorId(); - var elementData = this._elements.get(this._activeElement); - this._activeElement.classList.add('jsInvalidQuoteTarget'); - var icon = DomTraverse.childByClass(elementData.messageBodyEditor, 'icon'); - elRemove(icon); - var messageBody = elementData.messageBodyEditor; - var editor = elCreate('div'); - editor.className = 'editorContainer'; - //noinspection JSUnresolvedVariable - DomUtil.setInnerHtml(editor, data.returnValues.template); + _showEditor(data) { + const id = this._getEditorId(); + const activeElement = this._activeElement; + const elementData = this._elements.get(activeElement); + activeElement.classList.add("jsInvalidQuoteTarget"); + const icon = elementData.messageBodyEditor.querySelector(".icon"); + icon.remove(); + const messageBody = elementData.messageBodyEditor; + const editor = document.createElement("div"); + editor.className = "editorContainer"; + Util_1.default.setInnerHtml(editor, data.returnValues.template); messageBody.appendChild(editor); // bind buttons - var formSubmit = elBySel('.formSubmit', editor); - var buttonSave = elBySel('button[data-type="save"]', formSubmit); - buttonSave.addEventListener('click', this._save.bind(this)); - var buttonCancel = elBySel('button[data-type="cancel"]', formSubmit); - buttonCancel.addEventListener('click', this._restoreMessage.bind(this)); - EventHandler.add('com.woltlab.wcf.redactor', 'submitEditor_' + id, (function (data) { + const formSubmit = editor.querySelector(".formSubmit"); + const buttonSave = formSubmit.querySelector('button[data-type="save"]'); + buttonSave.addEventListener("click", () => this._save()); + const buttonCancel = formSubmit.querySelector('button[data-type="cancel"]'); + buttonCancel.addEventListener("click", () => this._restoreMessage()); + EventHandler.add("com.woltlab.wcf.redactor", `submitEditor_${id}`, (data) => { data.cancel = true; this._save(); - }).bind(this)); + }); // hide message header and footer - elHide(elementData.messageHeader); - elHide(elementData.messageFooter); - var editorElement = elById(id); - if (Environment.editor() === 'redactor') { - window.setTimeout((function () { + Util_1.default.hide(elementData.messageHeader); + Util_1.default.hide(elementData.messageFooter); + if (Environment.editor() === "redactor") { + window.setTimeout(() => { if (this._options.quoteManager) { this._options.quoteManager.setAlternativeEditor(id); } - UiScroll.element(this._activeElement); - }).bind(this), 250); + UiScroll.element(activeElement); + }, 250); } else { + const editorElement = document.getElementById(id); editorElement.focus(); } - }, + } /** * Restores the message view. - * - * @protected */ - _restoreMessage: function () { - var elementData = this._elements.get(this._activeElement); + _restoreMessage() { + const activeElement = this._activeElement; + const elementData = this._elements.get(activeElement); this._destroyEditor(); - elRemove(elementData.messageBodyEditor); + elementData.messageBodyEditor.remove(); elementData.messageBodyEditor = null; - elShow(elementData.messageBody); - elShow(elementData.messageFooter); - elShow(elementData.messageHeader); - this._activeElement.classList.remove('jsInvalidQuoteTarget'); + Util_1.default.show(elementData.messageBody); + Util_1.default.show(elementData.messageFooter); + Util_1.default.show(elementData.messageHeader); + activeElement.classList.remove("jsInvalidQuoteTarget"); this._activeElement = null; if (this._options.quoteManager) { this._options.quoteManager.clearAlternativeEditor(); } - }, + } /** * Saves the editor message. - * - * @protected */ - _save: function () { - var parameters = { + _save() { + const parameters = { containerID: this._options.containerId, data: { - message: '' + message: "", }, objectID: this._getObjectId(this._activeElement), - removeQuoteIDs: (this._options.quoteManager) ? this._options.quoteManager.getQuotesMarkedForRemoval() : [] + removeQuoteIDs: this._options.quoteManager ? this._options.quoteManager.getQuotesMarkedForRemoval() : [], }; - var id = this._getEditorId(); + const id = this._getEditorId(); // add any available settings - var settingsContainer = elById('settings_' + id); + const settingsContainer = document.getElementById(`settings_${id}`); 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', 'getText_' + id, parameters.data); - var validateResult = this._validate(parameters); + EventHandler.fire("com.woltlab.wcf.redactor2", `getText_${id}`, parameters.data); + let validateResult = this._validate(parameters); + // Legacy validation methods returned a plain boolean. if (!(validateResult instanceof Promise)) { if (validateResult === false) { validateResult = Promise.reject(); @@ -422,212 +366,182 @@ define([ validateResult = Promise.resolve(); } } - validateResult.then(function () { - EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_' + id, parameters); + validateResult.then(() => { + EventHandler.fire("com.woltlab.wcf.redactor2", `submit_${id}`, parameters); Ajax.api(this, { - actionName: 'save', - parameters: parameters + actionName: "save", + parameters: parameters, }); this._hideEditor(); - }.bind(this), function (e) { - console.log('Validation of post edit failed: ' + e); + }, (e) => { + const errorMessage = e.message; + console.log(`Validation of post edit failed: ${errorMessage}`); }); - }, + } /** * Validates the message and invokes listeners to perform additional validation. - * - * @param {Object} parameters request parameters - * @return {boolean} validation result - * @protected */ - _validate: function (parameters) { + _validate(parameters) { // remove all existing error elements - elBySelAll('.innerError', this._activeElement, elRemove); - var data = { + this._activeElement.querySelectorAll(".innerError").forEach((el) => el.remove()); + const data = { api: this, parameters: parameters, valid: true, - promises: [] + promises: [], }; - EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_' + this._getEditorId(), data); - data.promises.push(Promise[data.valid ? 'resolve' : 'reject']()); + EventHandler.fire("com.woltlab.wcf.redactor2", `validate_${this._getEditorId()}`, data); + if (data.valid) { + data.promises.push(Promise.resolve()); + } + else { + data.promises.push(Promise.reject()); + } return Promise.all(data.promises); - }, + } /** - * Throws an error by adding an inline error to target element. - * - * @param {Element} element erroneous element - * @param {string} message error message + * Throws an error by showing an inline error for the target element. */ - throwError: function (element, message) { - elInnerError(element, message); - }, + throwError(element, message) { + Util_1.default.innerError(element, message); + } /** * Shows the update message. - * - * @param {Object} data ajax response data - * @protected */ - _showMessage: function (data) { - var activeElement = this._activeElement; - var editorId = this._getEditorId(); - var elementData = this._elements.get(activeElement); - var attachmentLists = elBySelAll('.attachmentThumbnailList, .attachmentFileList', elementData.messageFooter); + _showMessage(data) { + const activeElement = this._activeElement; + const editorId = this._getEditorId(); + const elementData = this._elements.get(activeElement); // set new content - //noinspection JSUnresolvedVariable - DomUtil.setInnerHtml(DomTraverse.childByClass(elementData.messageBody, 'messageText'), data.returnValues.message); + Util_1.default.setInnerHtml(elementData.messageBody.querySelector(".messageText"), data.returnValues.message); // handle attachment list - //noinspection JSUnresolvedVariable - if (typeof data.returnValues.attachmentList === 'string') { - for (var i = 0, length = attachmentLists.length; i < length; i++) { - elRemove(attachmentLists[i]); - } - var element = elCreate('div'); - //noinspection JSUnresolvedVariable - DomUtil.setInnerHtml(element, data.returnValues.attachmentList); - var node; + if (typeof data.returnValues.attachmentList === "string") { + elementData.messageFooter + .querySelectorAll(".attachmentThumbnailList, .attachmentFileList") + .forEach((el) => el.remove()); + const element = document.createElement("div"); + Util_1.default.setInnerHtml(element, data.returnValues.attachmentList); + let node; while (element.childNodes.length) { node = element.childNodes[element.childNodes.length - 1]; elementData.messageFooter.insertBefore(node, elementData.messageFooter.firstChild); } } - // handle poll - //noinspection JSUnresolvedVariable - if (typeof data.returnValues.poll === 'string') { - // find current poll - var poll = elBySel('.pollContainer', elementData.messageBody); + if (typeof data.returnValues.poll === "string") { + const poll = elementData.messageBody.querySelector(".pollContainer"); if (poll !== null) { - // poll contain is wrapped inside `.jsInlineEditorHideContent` - elRemove(poll.parentNode); + // The poll container is wrapped inside `.jsInlineEditorHideContent`. + poll.parentElement.remove(); } - var pollContainer = elCreate('div'); - pollContainer.className = 'jsInlineEditorHideContent'; - //noinspection JSUnresolvedVariable - DomUtil.setInnerHtml(pollContainer, data.returnValues.poll); - DomUtil.prepend(pollContainer, elementData.messageBody); + const pollContainer = document.createElement("div"); + pollContainer.className = "jsInlineEditorHideContent"; + Util_1.default.setInnerHtml(pollContainer, data.returnValues.poll); + elementData.messageBody.insertAdjacentElement("afterbegin", pollContainer); } this._restoreMessage(); this._updateHistory(this._getHash(this._getObjectId(activeElement))); - EventHandler.fire('com.woltlab.wcf.redactor', 'autosaveDestroy_' + editorId); + EventHandler.fire("com.woltlab.wcf.redactor", `autosaveDestroy_${editorId}`); UiNotification.show(); if (this._options.quoteManager) { this._options.quoteManager.clearAlternativeEditor(); this._options.quoteManager.countQuotes(); } - }, + } /** * Hides the editor from view. - * - * @protected */ - _hideEditor: function () { - var elementData = this._elements.get(this._activeElement); - elHide(DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer')); - var icon = elCreate('span'); - icon.className = 'icon icon48 fa-spinner'; + _hideEditor() { + const elementData = this._elements.get(this._activeElement); + const editorContainer = elementData.messageBodyEditor.querySelector(".editorContainer"); + Util_1.default.hide(editorContainer); + const icon = document.createElement("span"); + icon.className = "icon icon48 fa-spinner"; elementData.messageBodyEditor.appendChild(icon); - }, + } /** * Restores the previously hidden editor. - * - * @protected */ - _restoreEditor: function () { - var elementData = this._elements.get(this._activeElement); - var icon = elBySel('.fa-spinner', elementData.messageBodyEditor); - elRemove(icon); - var editorContainer = DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer'); - if (editorContainer !== null) - elShow(editorContainer); - }, + _restoreEditor() { + const elementData = this._elements.get(this._activeElement); + const messageBodyEditor = elementData.messageBodyEditor; + const icon = messageBodyEditor.querySelector(".fa-spinner"); + icon.remove(); + const editorContainer = messageBodyEditor.querySelector(".editorContainer"); + if (editorContainer !== null) { + Util_1.default.show(editorContainer); + } + } /** * Destroys the editor instance. - * - * @protected */ - _destroyEditor: function () { - EventHandler.fire('com.woltlab.wcf.redactor2', 'autosaveDestroy_' + this._getEditorId()); - EventHandler.fire('com.woltlab.wcf.redactor2', 'destroy_' + this._getEditorId()); - }, + _destroyEditor() { + EventHandler.fire("com.woltlab.wcf.redactor2", `autosaveDestroy_${this._getEditorId()}`); + EventHandler.fire("com.woltlab.wcf.redactor2", `destroy_${this._getEditorId()}`); + } /** * Returns the hash added to the url after successfully editing a message. - * - * @param {int} objectId message object id - * @return string - * @protected */ - _getHash: function (objectId) { - return '#message' + objectId; - }, + _getHash(objectId) { + return `#message${objectId}`; + } /** * Updates the history to avoid old content when going back in the browser * history. - * - * @param {string} hash location hash - * @protected */ - _updateHistory: function (hash) { + _updateHistory(hash) { window.location.hash = hash; - }, + } /** * Returns the unique editor id. - * - * @return {string} editor id - * @protected */ - _getEditorId: function () { - return this._options.editorPrefix + this._getObjectId(this._activeElement); - }, + _getEditorId() { + return this._options.editorPrefix + this._getObjectId(this._activeElement).toString(); + } /** * Returns the element's `data-object-id` value. - * - * @param {Element} element target element - * @return {int} - * @protected */ - _getObjectId: function (element) { - return ~~elData(element, 'object-id'); - }, - _ajaxFailure: function (data) { - var elementData = this._elements.get(this._activeElement); - var editor = elBySel('.redactor-layer', elementData.messageBodyEditor); + _getObjectId(element) { + return ~~(element.dataset.objectId || ""); + } + _ajaxFailure(data) { + const elementData = this._elements.get(this._activeElement); + const editor = elementData.messageBodyEditor.querySelector(".redactor-layer"); // handle errors occurring on editor load if (editor === null) { this._restoreMessage(); return true; } this._restoreEditor(); - //noinspection JSUnresolvedVariable if (!data || data.returnValues === undefined || data.returnValues.realErrorMessage === undefined) { return true; } - //noinspection JSUnresolvedVariable - elInnerError(editor, data.returnValues.realErrorMessage); + Util_1.default.innerError(editor, data.returnValues.realErrorMessage); return false; - }, - _ajaxSuccess: function (data) { + } + _ajaxSuccess(data) { switch (data.actionName) { - case 'beginEdit': + case "beginEdit": this._showEditor(data); break; - case 'save': + case "save": this._showMessage(data); break; } - }, - _ajaxSetup: function () { + } + _ajaxSetup() { return { data: { className: this._options.className, - interfaceName: 'wcf\\data\\IMessageInlineEditorAction' + interfaceName: "wcf\\data\\IMessageInlineEditorAction", }, - silent: true + silent: true, }; - }, - /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */ - legacyEdit: function (containerId) { - this._click(elById(containerId), null); } - }; + /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */ + legacyEdit(containerId) { + this._click(document.getElementById(containerId), null); + } + } + Core.enableLegacyInheritance(UiMessageInlineEditor); return UiMessageInlineEditor; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.js deleted file mode 100644 index 017e49a663..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.js +++ /dev/null @@ -1,757 +0,0 @@ -/** - * Flexible message inline editor. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Ui/Message/InlineEditor - */ -define( - [ - 'Ajax', 'Core', 'Dictionary', 'Environment', - 'EventHandler', 'Language', 'ObjectMap', 'Dom/ChangeListener', 'Dom/Traverse', - 'Dom/Util', 'Ui/Notification', 'Ui/ReusableDropdown', 'WoltLabSuite/Core/Ui/Scroll' - ], - function( - Ajax, Core, Dictionary, Environment, - EventHandler, Language, ObjectMap, DomChangeListener, DomTraverse, - DomUtil, UiNotification, UiReusableDropdown, UiScroll - ) -{ - "use strict"; - - if (!COMPILER_TARGET_DEFAULT) { - var Fake = function() {}; - Fake.prototype = { - init: function() {}, - rebuild: function() {}, - _click: function() {}, - _clickDropdown: function() {}, - _dropdownBuild: function() {}, - _dropdownToggle: function() {}, - _dropdownGetItems: function() {}, - _dropdownOpen: function() {}, - _dropdownSelect: function() {}, - _clickDropdownItem: function() {}, - _prepare: function() {}, - _showEditor: function() {}, - _restoreMessage: function() {}, - _save: function() {}, - _validate: function() {}, - throwError: function() {}, - _showMessage: function() {}, - _hideEditor: function() {}, - _restoreEditor: function() {}, - _destroyEditor: function() {}, - _getHash: function() {}, - _updateHistory: function() {}, - _getEditorId: function() {}, - _getObjectId: function() {}, - _ajaxFailure: function() {}, - _ajaxSuccess: function() {}, - _ajaxSetup: function() {}, - legacyEdit: function() {} - }; - return Fake; - } - - /** - * @constructor - */ - function UiMessageInlineEditor(options) { this.init(options); } - UiMessageInlineEditor.prototype = { - /** - * Initializes the message inline editor. - * - * @param {Object} options list of configuration options - */ - init: function(options) { - this._activeDropdownElement = null; - this._activeElement = null; - this._dropdownMenu = null; - this._elements = new ObjectMap(); - this._options = Core.extend({ - canEditInline: false, - - className: '', - containerId: 0, - dropdownIdentifier: '', - editorPrefix: 'messageEditor', - - messageSelector: '.jsMessage', - - quoteManager: null - }, options); - - this.rebuild(); - - DomChangeListener.add('Ui/Message/InlineEdit_' + this._options.className, this.rebuild.bind(this)); - }, - - /** - * Initializes each applicable message, should be called whenever new - * messages are being displayed. - */ - rebuild: function() { - var button, canEdit, element, elements = elBySelAll(this._options.messageSelector); - - for (var i = 0, length = elements.length; i < length; i++) { - element = elements[i]; - if (this._elements.has(element)) { - continue; - } - - button = elBySel('.jsMessageEditButton', element); - if (button !== null) { - canEdit = elDataBool(element, 'can-edit'); - - if (this._options.canEditInline || elDataBool(element, 'can-edit-inline')) { - button.addEventListener('click', this._clickDropdown.bind(this, element)); - button.classList.add('jsDropdownEnabled'); - - if (canEdit) { - button.addEventListener('dblclick', this._click.bind(this, element)); - } - } - else if (canEdit) { - button.addEventListener('click', this._click.bind(this, element)); - } - } - - var messageBody = elBySel('.messageBody', element); - var messageFooter = elBySel('.messageFooter', element); - var messageHeader = elBySel('.messageHeader', element); - - this._elements.set(element, { - button: button, - messageBody: messageBody, - messageBodyEditor: null, - messageFooter: messageFooter, - messageFooterButtons: elBySel('.messageFooterButtons', messageFooter), - messageHeader: messageHeader, - messageText: elBySel('.messageText', messageBody) - }); - } - }, - - /** - * Handles clicks on the edit button or the edit dropdown item. - * - * @param {Element} element message element - * @param {?Event} event event object - * @protected - */ - _click: function(element, event) { - if (element === null) element = this._activeDropdownElement; - if (event) event.preventDefault(); - - if (this._activeElement === null) { - this._activeElement = element; - - this._prepare(); - - Ajax.api(this, { - actionName: 'beginEdit', - parameters: { - containerID: this._options.containerId, - objectID: this._getObjectId(element) - } - }); - } - else { - UiNotification.show('wcf.message.error.editorAlreadyInUse', null, 'warning'); - } - }, - - /** - * Creates and opens the dropdown on first usage. - * - * @param {Element} element message element - * @param {Object} event event object - * @protected - */ - _clickDropdown: function(element, event) { - event.preventDefault(); - - var button = event.currentTarget; - if (button.classList.contains('dropdownToggle')) { - return; - } - - button.classList.add('dropdownToggle'); - button.parentNode.classList.add('dropdown'); - (function(button, element) { - button.addEventListener('click', (function(event) { - event.preventDefault(); - event.stopPropagation(); - - this._activeDropdownElement = element; - UiReusableDropdown.toggleDropdown(this._options.dropdownIdentifier, button); - }).bind(this)); - }).bind(this)(button, element); - - // build dropdown - if (this._dropdownMenu === null) { - this._dropdownMenu = elCreate('ul'); - this._dropdownMenu.className = 'dropdownMenu'; - - var items = this._dropdownGetItems(); - - EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownInit_' + this._options.dropdownIdentifier, { - items: items - }); - - this._dropdownBuild(items); - - UiReusableDropdown.init(this._options.dropdownIdentifier, this._dropdownMenu); - UiReusableDropdown.registerCallback(this._options.dropdownIdentifier, this._dropdownToggle.bind(this)); - } - - setTimeout(function() { - Core.triggerEvent(button, 'click'); - }, 10); - }, - - /** - * Creates the dropdown menu on first usage. - * - * @param {Object} items list of dropdown items - * @protected - */ - _dropdownBuild: function(items) { - var item, label, listItem; - var callbackClick = this._clickDropdownItem.bind(this); - - for (var i = 0, length = items.length; i < length; i++) { - item = items[i]; - listItem = elCreate('li'); - elData(listItem, 'item', item.item); - - if (item.item === 'divider') { - listItem.className = 'dropdownDivider'; - } - else { - label = elCreate('span'); - label.textContent = Language.get(item.label); - listItem.appendChild(label); - - if (item.item === 'editItem') { - listItem.addEventListener('click', this._click.bind(this, null)); - } - else { - listItem.addEventListener('click', callbackClick); - } - } - - this._dropdownMenu.appendChild(listItem); - } - }, - - /** - * Callback for dropdown toggle. - * - * @param {int} containerId container id - * @param {string} action toggle action, either 'open' or 'close' - * @protected - */ - _dropdownToggle: function(containerId, action) { - var elementData = this._elements.get(this._activeDropdownElement); - elementData.button.parentNode.classList[(action === 'open' ? 'add' : 'remove')]('dropdownOpen'); - elementData.messageFooterButtons.classList[(action === 'open' ? 'add' : 'remove')]('forceVisible'); - - if (action === 'open') { - var visibility = this._dropdownOpen(); - - EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownOpen_' + this._options.dropdownIdentifier, { - element: this._activeDropdownElement, - visibility: visibility - }); - - var item, listItem, visiblePredecessor = false; - for (var i = 0; i < this._dropdownMenu.childElementCount; i++) { - listItem = this._dropdownMenu.children[i]; - item = elData(listItem, 'item'); - - if (item === 'divider') { - if (visiblePredecessor) { - elShow(listItem); - - visiblePredecessor = false; - } - else { - elHide(listItem); - } - } - else { - if (objOwns(visibility, item) && visibility[item] === false) { - elHide(listItem); - - // check if previous item was a divider - if (i > 0 && i + 1 === this._dropdownMenu.childElementCount) { - if (elData(listItem.previousElementSibling, 'item') === 'divider') { - elHide(listItem.previousElementSibling); - } - } - } - else { - elShow(listItem); - - visiblePredecessor = true; - } - } - } - } - }, - - /** - * Returns the list of dropdown items for this type. - * - * @return {Array} list of objects containing the type name and label - * @protected - */ - _dropdownGetItems: function() {}, - - /** - * Invoked once the dropdown for this type is shown, expects a list of type name and a boolean value - * to represent the visibility of each item. Items that do not appear in this list will be considered - * visible. - * - * @return {Object} - * @protected - */ - _dropdownOpen: function() {}, - - /** - * Invoked whenever the user selects an item from the dropdown menu, the selected item is passed as argument. - * - * @param {string} item selected dropdown item - * @protected - */ - _dropdownSelect: function(item) {}, - - /** - * Handles clicks on a dropdown item. - * - * @param {Event} event event object - * @protected - */ - _clickDropdownItem: function(event) { - event.preventDefault(); - - //noinspection JSCheckFunctionSignatures - var item = elData(event.currentTarget, 'item'); - var data = { - cancel: false, - element: this._activeDropdownElement, - item: item - }; - EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownItemClick_' + this._options.dropdownIdentifier, data); - - if (data.cancel === true) { - event.preventDefault(); - } - else { - this._dropdownSelect(item); - } - }, - - /** - * Prepares the message for editor display. - * - * @protected - */ - _prepare: function() { - var data = this._elements.get(this._activeElement); - - var messageBodyEditor = elCreate('div'); - messageBodyEditor.className = 'messageBody editor'; - data.messageBodyEditor = messageBodyEditor; - - var icon = elCreate('span'); - icon.className = 'icon icon48 fa-spinner'; - messageBodyEditor.appendChild(icon); - - DomUtil.insertAfter(messageBodyEditor, data.messageBody); - - elHide(data.messageBody); - }, - - /** - * Shows the message editor. - * - * @param {Object} data ajax response data - * @protected - */ - _showEditor: function(data) { - var id = this._getEditorId(); - var elementData = this._elements.get(this._activeElement); - - this._activeElement.classList.add('jsInvalidQuoteTarget'); - var icon = DomTraverse.childByClass(elementData.messageBodyEditor, 'icon'); - elRemove(icon); - - var messageBody = elementData.messageBodyEditor; - var editor = elCreate('div'); - editor.className = 'editorContainer'; - //noinspection JSUnresolvedVariable - DomUtil.setInnerHtml(editor, data.returnValues.template); - messageBody.appendChild(editor); - - // bind buttons - var formSubmit = elBySel('.formSubmit', editor); - - var buttonSave = elBySel('button[data-type="save"]', formSubmit); - buttonSave.addEventListener('click', this._save.bind(this)); - - var buttonCancel = elBySel('button[data-type="cancel"]', formSubmit); - buttonCancel.addEventListener('click', this._restoreMessage.bind(this)); - - EventHandler.add('com.woltlab.wcf.redactor', 'submitEditor_' + id, (function(data) { - data.cancel = true; - - this._save(); - }).bind(this)); - - // hide message header and footer - elHide(elementData.messageHeader); - elHide(elementData.messageFooter); - - var editorElement = elById(id); - if (Environment.editor() === 'redactor') { - window.setTimeout((function() { - if (this._options.quoteManager) { - this._options.quoteManager.setAlternativeEditor(id); - } - - UiScroll.element(this._activeElement); - }).bind(this), 250); - } - else { - editorElement.focus(); - } - }, - - /** - * Restores the message view. - * - * @protected - */ - _restoreMessage: function() { - var elementData = this._elements.get(this._activeElement); - - this._destroyEditor(); - - elRemove(elementData.messageBodyEditor); - elementData.messageBodyEditor = null; - - elShow(elementData.messageBody); - elShow(elementData.messageFooter); - elShow(elementData.messageHeader); - this._activeElement.classList.remove('jsInvalidQuoteTarget'); - - this._activeElement = null; - - if (this._options.quoteManager) { - this._options.quoteManager.clearAlternativeEditor(); - } - }, - - /** - * Saves the editor message. - * - * @protected - */ - _save: function() { - var parameters = { - containerID: this._options.containerId, - data: { - message: '' - }, - objectID: this._getObjectId(this._activeElement), - removeQuoteIDs: (this._options.quoteManager) ? this._options.quoteManager.getQuotesMarkedForRemoval() : [] - }; - - var id = this._getEditorId(); - - // add any available settings - var settingsContainer = elById('settings_' + id); - 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', 'getText_' + id, parameters.data); - - var validateResult = this._validate(parameters); - - if (!(validateResult instanceof Promise)) { - if (validateResult === false) { - validateResult = Promise.reject(); - } - else { - validateResult = Promise.resolve(); - } - } - - validateResult.then(function () { - EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_' + id, parameters); - - Ajax.api(this, { - actionName: 'save', - parameters: parameters - }); - - this._hideEditor(); - }.bind(this), function(e) { - console.log('Validation of post edit failed: '+ e); - }); - }, - - /** - * Validates the message and invokes listeners to perform additional validation. - * - * @param {Object} parameters request parameters - * @return {boolean} validation result - * @protected - */ - _validate: function(parameters) { - // remove all existing error elements - elBySelAll('.innerError', this._activeElement, elRemove); - - var data = { - api: this, - parameters: parameters, - valid: true, - promises: [] - }; - - EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_' + this._getEditorId(), data); - - data.promises.push(Promise[data.valid ? 'resolve' : 'reject']()); - - return Promise.all(data.promises); - }, - - /** - * 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); - }, - - /** - * Shows the update message. - * - * @param {Object} data ajax response data - * @protected - */ - _showMessage: function(data) { - var activeElement = this._activeElement; - var editorId = this._getEditorId(); - var elementData = this._elements.get(activeElement); - var attachmentLists = elBySelAll('.attachmentThumbnailList, .attachmentFileList', elementData.messageFooter); - - // set new content - //noinspection JSUnresolvedVariable - DomUtil.setInnerHtml(DomTraverse.childByClass(elementData.messageBody, 'messageText'), data.returnValues.message); - - // handle attachment list - //noinspection JSUnresolvedVariable - if (typeof data.returnValues.attachmentList === 'string') { - for (var i = 0, length = attachmentLists.length; i < length; i++) { - elRemove(attachmentLists[i]); - } - - var element = elCreate('div'); - //noinspection JSUnresolvedVariable - DomUtil.setInnerHtml(element, data.returnValues.attachmentList); - - var node; - while (element.childNodes.length) { - node = element.childNodes[element.childNodes.length - 1]; - elementData.messageFooter.insertBefore(node, elementData.messageFooter.firstChild); - } - } - - // handle poll - //noinspection JSUnresolvedVariable - if (typeof data.returnValues.poll === 'string') { - // find current poll - var poll = elBySel('.pollContainer', elementData.messageBody); - if (poll !== null) { - // poll contain is wrapped inside `.jsInlineEditorHideContent` - elRemove(poll.parentNode); - } - - var pollContainer = elCreate('div'); - pollContainer.className = 'jsInlineEditorHideContent'; - //noinspection JSUnresolvedVariable - DomUtil.setInnerHtml(pollContainer, data.returnValues.poll); - - DomUtil.prepend(pollContainer, elementData.messageBody); - } - - this._restoreMessage(); - - this._updateHistory(this._getHash(this._getObjectId(activeElement))); - - EventHandler.fire('com.woltlab.wcf.redactor', 'autosaveDestroy_' + editorId); - - UiNotification.show(); - - if (this._options.quoteManager) { - this._options.quoteManager.clearAlternativeEditor(); - this._options.quoteManager.countQuotes(); - } - }, - - /** - * Hides the editor from view. - * - * @protected - */ - _hideEditor: function() { - var elementData = this._elements.get(this._activeElement); - elHide(DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer')); - - var icon = elCreate('span'); - icon.className = 'icon icon48 fa-spinner'; - elementData.messageBodyEditor.appendChild(icon); - }, - - /** - * Restores the previously hidden editor. - * - * @protected - */ - _restoreEditor: function() { - var elementData = this._elements.get(this._activeElement); - var icon = elBySel('.fa-spinner', elementData.messageBodyEditor); - elRemove(icon); - - var editorContainer = DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer'); - if (editorContainer !== null) elShow(editorContainer); - }, - - /** - * Destroys the editor instance. - * - * @protected - */ - _destroyEditor: function() { - EventHandler.fire('com.woltlab.wcf.redactor2', 'autosaveDestroy_' + this._getEditorId()); - EventHandler.fire('com.woltlab.wcf.redactor2', 'destroy_' + this._getEditorId()); - }, - - /** - * Returns the hash added to the url after successfully editing a message. - * - * @param {int} objectId message object id - * @return string - * @protected - */ - _getHash: function(objectId) { - return '#message' + objectId; - }, - - /** - * Updates the history to avoid old content when going back in the browser - * history. - * - * @param {string} hash location hash - * @protected - */ - _updateHistory: function(hash) { - window.location.hash = hash; - }, - - /** - * Returns the unique editor id. - * - * @return {string} editor id - * @protected - */ - _getEditorId: function() { - return this._options.editorPrefix + this._getObjectId(this._activeElement); - }, - - /** - * Returns the element's `data-object-id` value. - * - * @param {Element} element target element - * @return {int} - * @protected - */ - _getObjectId: function(element) { - return ~~elData(element, 'object-id'); - }, - - _ajaxFailure: function(data) { - var elementData = this._elements.get(this._activeElement); - var editor = elBySel('.redactor-layer', elementData.messageBodyEditor); - - // handle errors occurring on editor load - if (editor === null) { - this._restoreMessage(); - - return true; - } - - this._restoreEditor(); - - //noinspection JSUnresolvedVariable - if (!data || data.returnValues === undefined || data.returnValues.realErrorMessage === undefined) { - return true; - } - - //noinspection JSUnresolvedVariable - elInnerError(editor, data.returnValues.realErrorMessage); - - return false; - }, - - _ajaxSuccess: function(data) { - switch (data.actionName) { - case 'beginEdit': - this._showEditor(data); - break; - - case 'save': - this._showMessage(data); - break; - } - }, - - _ajaxSetup: function() { - return { - data: { - className: this._options.className, - interfaceName: 'wcf\\data\\IMessageInlineEditorAction' - }, - silent: true - }; - }, - - /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */ - legacyEdit: function(containerId) { - this._click(elById(containerId), null); - } - }; - - return UiMessageInlineEditor; -}); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.ts new file mode 100644 index 0000000000..af75d13a82 --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Message/InlineEditor.ts @@ -0,0 +1,723 @@ +/** + * Flexible message inline editor. + * + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/Message/InlineEditor + */ + +import * as Ajax from "../../Ajax"; +import { AjaxCallbackObject, AjaxCallbackSetup, ResponseData } from "../../Ajax/Data"; +import * as Core from "../../Core"; +import DomChangeListener from "../../Dom/Change/Listener"; +import DomUtil from "../../Dom/Util"; +import * as Environment from "../../Environment"; +import * as EventHandler from "../../Event/Handler"; +import * as Language from "../../Language"; +import { NotificationAction } from "../Dropdown/Data"; +import * as UiDropdownReusable from "../Dropdown/Reusable"; +import * as UiNotification from "../Notification"; +import * as UiScroll from "../Scroll"; + +interface MessageInlineEditorOptions { + canEditInline: boolean; + + className: string; + containerId: number; + dropdownIdentifier: string; + editorPrefix: string; + + messageSelector: string; + + // This is the legacy jQuery based class. + quoteManager: any; +} + +interface ElementData { + button: HTMLAnchorElement; + messageBody: HTMLElement; + messageBodyEditor: HTMLElement | null; + messageFooter: HTMLElement; + messageFooterButtons: HTMLUListElement; + messageHeader: HTMLElement; + messageText: HTMLElement; +} + +interface ItemData { + item: "divider" | "editItem" | string; + label?: string; +} + +interface ElementVisibility { + [key: string]: boolean; +} + +interface ValidationData { + api: UiMessageInlineEditor; + parameters: ArbitraryObject; + valid: boolean; + promises: Promise[]; +} + +interface AjaxResponseEditor extends ResponseData { + returnValues: { + template: string; + }; +} + +interface AjaxResponseMessage extends ResponseData { + returnValues: { + attachmentList?: string; + message: string; + poll?: string; + }; +} + +class UiMessageInlineEditor implements AjaxCallbackObject { + protected _activeDropdownElement: HTMLElement | null = null; + protected _activeElement: HTMLElement | null = null; + protected _dropdownMenu: HTMLUListElement | null = null; + protected readonly _elements = new WeakMap(); + protected readonly _options: MessageInlineEditorOptions; + + /** + * Initializes the message inline editor. + */ + constructor(opts: Partial) { + this._options = Core.extend( + { + canEditInline: false, + + className: "", + containerId: 0, + dropdownIdentifier: "", + editorPrefix: "messageEditor", + + messageSelector: ".jsMessage", + + quoteManager: null, + }, + opts, + ) as MessageInlineEditorOptions; + + this.rebuild(); + + DomChangeListener.add(`Ui/Message/InlineEdit_${this._options.className}`, () => this.rebuild()); + } + + /** + * Initializes each applicable message, should be called whenever new + * messages are being displayed. + */ + rebuild(): void { + document.querySelectorAll(this._options.messageSelector).forEach((element: HTMLElement) => { + if (this._elements.has(element)) { + return; + } + + const button = element.querySelector(".jsMessageEditButton") as HTMLAnchorElement; + if (button !== null) { + const canEdit = Core.stringToBool(element.dataset.canEdit || ""); + const canEditInline = Core.stringToBool(element.dataset.canEditInline || ""); + + if (this._options.canEditInline || canEditInline) { + button.addEventListener("click", (ev) => this._clickDropdown(element, ev)); + button.classList.add("jsDropdownEnabled"); + + if (canEdit) { + button.addEventListener("dblclick", (ev) => this._click(element, ev)); + } + } else if (canEdit) { + button.addEventListener("click", (ev) => this._click(element, ev)); + } + } + + const messageBody = element.querySelector(".messageBody") as HTMLElement; + const messageFooter = element.querySelector(".messageFooter") as HTMLElement; + const messageFooterButtons = messageFooter.querySelector(".messageFooterButtons") as HTMLUListElement; + const messageHeader = element.querySelector(".messageHeader") as HTMLElement; + const messageText = messageBody.querySelector(".messageText") as HTMLElement; + + this._elements.set(element, { + button, + messageBody, + messageBodyEditor: null, + messageFooter, + messageFooterButtons, + messageHeader, + messageText, + }); + }); + } + + /** + * Handles clicks on the edit button or the edit dropdown item. + */ + protected _click(element: HTMLElement | null, event: MouseEvent | null): void { + if (element === null) { + element = this._activeDropdownElement; + } + if (event) { + event.preventDefault(); + } + + if (this._activeElement === null) { + this._activeElement = element; + + this._prepare(); + + Ajax.api(this, { + actionName: "beginEdit", + parameters: { + containerID: this._options.containerId, + objectID: this._getObjectId(element!), + }, + }); + } else { + UiNotification.show("wcf.message.error.editorAlreadyInUse", undefined, "warning"); + } + } + + /** + * Creates and opens the dropdown on first usage. + */ + protected _clickDropdown(element: HTMLElement, event: MouseEvent): void { + event.preventDefault(); + + const button = event.currentTarget as HTMLElement; + if (button.classList.contains("dropdownToggle")) { + return; + } + + button.classList.add("dropdownToggle"); + button.parentElement!.classList.add("dropdown"); + button.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + + this._activeDropdownElement = element; + UiDropdownReusable.toggleDropdown(this._options.dropdownIdentifier, button); + }); + + // build dropdown + if (this._dropdownMenu === null) { + this._dropdownMenu = document.createElement("ul"); + this._dropdownMenu.className = "dropdownMenu"; + + const items = this._dropdownGetItems(); + + EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownInit_${this._options.dropdownIdentifier}`, { + items: items, + }); + + this._dropdownBuild(items); + + UiDropdownReusable.init(this._options.dropdownIdentifier, this._dropdownMenu); + UiDropdownReusable.registerCallback(this._options.dropdownIdentifier, (containerId, action) => + this._dropdownToggle(containerId, action), + ); + } + + setTimeout(() => button.click(), 10); + } + + /** + * Creates the dropdown menu on first usage. + */ + protected _dropdownBuild(items: ItemData[]): void { + items.forEach((item) => { + const listItem = document.createElement("li"); + listItem.dataset.item = item.item; + + if (item.item === "divider") { + listItem.className = "dropdownDivider"; + } else { + const label = document.createElement("span"); + label.textContent = Language.get(item.label!); + listItem.appendChild(label); + + if (item.item === "editItem") { + listItem.addEventListener("click", (ev) => this._click(null, ev)); + } else { + listItem.addEventListener("click", (ev) => this._clickDropdownItem(ev)); + } + } + + this._dropdownMenu!.appendChild(listItem); + }); + } + + /** + * Callback for dropdown toggle. + */ + protected _dropdownToggle(containerId: string, action: NotificationAction): void { + const elementData = this._elements.get(this._activeDropdownElement!)!; + const buttonParent = elementData.button.parentElement!; + + if (action === "close") { + buttonParent.classList.remove("dropdownOpen"); + elementData.messageFooterButtons.classList.remove("forceVisible"); + + return; + } + + buttonParent.classList.add("dropdownOpen"); + elementData.messageFooterButtons.classList.add("forceVisible"); + + const visibility = new Map(Object.entries(this._dropdownOpen())); + + EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownOpen_${this._options.dropdownIdentifier}`, { + element: this._activeDropdownElement, + visibility, + }); + + const dropdownMenu = this._dropdownMenu!; + + let visiblePredecessor = false; + const children = Array.from(dropdownMenu.children); + children.forEach((listItem: HTMLElement, index) => { + const item = listItem.dataset.item!; + + if (item === "divider") { + if (visiblePredecessor) { + DomUtil.show(listItem); + + visiblePredecessor = false; + } else { + DomUtil.hide(listItem); + } + } else { + if (visibility.get(item) === false) { + DomUtil.hide(listItem); + + // check if previous item was a divider + if (index > 0 && index + 1 === children.length) { + const previousElementSibling = listItem.previousElementSibling as HTMLElement; + if (previousElementSibling.dataset.item === "divider") { + DomUtil.hide(previousElementSibling); + } + } + } else { + DomUtil.show(listItem); + + visiblePredecessor = true; + } + } + }); + } + + /** + * Returns the list of dropdown items for this type. + */ + protected _dropdownGetItems(): ItemData[] { + // This should be an abstract method, but cannot be marked as such for backwards compatibility. + return []; + } + + /** + * Invoked once the dropdown for this type is shown, expects a list of type name and a boolean value + * to represent the visibility of each item. Items that do not appear in this list will be considered + * visible. + */ + protected _dropdownOpen(): ElementVisibility { + // This should be an abstract method, but cannot be marked as such for backwards compatibility. + return {}; + } + + /** + * Invoked whenever the user selects an item from the dropdown menu, the selected item is passed as argument. + */ + protected _dropdownSelect(_item: string): void { + // This should be an abstract method, but cannot be marked as such for backwards compatibility. + } + + /** + * Handles clicks on a dropdown item. + */ + protected _clickDropdownItem(event: MouseEvent): void { + event.preventDefault(); + + const target = event.currentTarget as HTMLElement; + const item = target.dataset.item!; + const data = { + cancel: false, + element: this._activeDropdownElement, + item, + }; + EventHandler.fire("com.woltlab.wcf.inlineEditor", `dropdownItemClick_${this._options.dropdownIdentifier}`, data); + + if (data.cancel) { + event.preventDefault(); + } else { + this._dropdownSelect(item); + } + } + + /** + * Prepares the message for editor display. + */ + protected _prepare(): void { + const data = this._elements.get(this._activeElement!)!; + + const messageBodyEditor = document.createElement("div"); + messageBodyEditor.className = "messageBody editor"; + data.messageBodyEditor = messageBodyEditor; + + const icon = document.createElement("span"); + icon.className = "icon icon48 fa-spinner"; + messageBodyEditor.appendChild(icon); + + data.messageBody.insertAdjacentElement("afterend", messageBodyEditor); + + DomUtil.hide(data.messageBody); + } + + /** + * Shows the message editor. + */ + protected _showEditor(data: AjaxResponseEditor): void { + const id = this._getEditorId(); + const activeElement = this._activeElement!; + const elementData = this._elements.get(activeElement)!; + + activeElement.classList.add("jsInvalidQuoteTarget"); + const icon = elementData.messageBodyEditor!.querySelector(".icon") as HTMLElement; + icon.remove(); + + const messageBody = elementData.messageBodyEditor!; + const editor = document.createElement("div"); + editor.className = "editorContainer"; + DomUtil.setInnerHtml(editor, data.returnValues.template); + messageBody.appendChild(editor); + + // bind buttons + const formSubmit = editor.querySelector(".formSubmit") as HTMLElement; + + const buttonSave = formSubmit.querySelector('button[data-type="save"]') as HTMLButtonElement; + buttonSave.addEventListener("click", () => this._save()); + + const buttonCancel = formSubmit.querySelector('button[data-type="cancel"]') as HTMLButtonElement; + buttonCancel.addEventListener("click", () => this._restoreMessage()); + + EventHandler.add("com.woltlab.wcf.redactor", `submitEditor_${id}`, (data: { cancel: boolean }) => { + data.cancel = true; + + this._save(); + }); + + // hide message header and footer + DomUtil.hide(elementData.messageHeader); + DomUtil.hide(elementData.messageFooter); + + if (Environment.editor() === "redactor") { + window.setTimeout(() => { + if (this._options.quoteManager) { + this._options.quoteManager.setAlternativeEditor(id); + } + + UiScroll.element(activeElement); + }, 250); + } else { + const editorElement = document.getElementById(id) as HTMLElement; + editorElement.focus(); + } + } + + /** + * Restores the message view. + */ + protected _restoreMessage(): void { + const activeElement = this._activeElement!; + const elementData = this._elements.get(activeElement)!; + + this._destroyEditor(); + + elementData.messageBodyEditor!.remove(); + elementData.messageBodyEditor = null; + + DomUtil.show(elementData.messageBody); + DomUtil.show(elementData.messageFooter); + DomUtil.show(elementData.messageHeader); + activeElement.classList.remove("jsInvalidQuoteTarget"); + + this._activeElement = null; + + if (this._options.quoteManager) { + this._options.quoteManager.clearAlternativeEditor(); + } + } + + /** + * Saves the editor message. + */ + protected _save(): void { + const parameters = { + containerID: this._options.containerId, + data: { + message: "", + }, + objectID: this._getObjectId(this._activeElement!), + removeQuoteIDs: this._options.quoteManager ? this._options.quoteManager.getQuotesMarkedForRemoval() : [], + }; + + const id = this._getEditorId(); + + // add any available settings + const settingsContainer = document.getElementById(`settings_${id}`); + 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", `getText_${id}`, parameters.data); + + let validateResult: unknown = this._validate(parameters); + + // Legacy validation methods returned a plain boolean. + if (!(validateResult instanceof Promise)) { + if (validateResult === false) { + validateResult = Promise.reject(); + } else { + validateResult = Promise.resolve(); + } + } + + (validateResult as Promise).then( + () => { + EventHandler.fire("com.woltlab.wcf.redactor2", `submit_${id}`, parameters); + + Ajax.api(this, { + actionName: "save", + parameters: parameters, + }); + + this._hideEditor(); + }, + (e) => { + const errorMessage = (e as Error).message; + console.log(`Validation of post edit failed: ${errorMessage}`); + }, + ); + } + + /** + * Validates the message and invokes listeners to perform additional validation. + */ + protected _validate(parameters: ArbitraryObject): Promise { + // remove all existing error elements + this._activeElement!.querySelectorAll(".innerError").forEach((el) => el.remove()); + + const data: ValidationData = { + api: this, + parameters: parameters, + valid: true, + promises: [], + }; + + EventHandler.fire("com.woltlab.wcf.redactor2", `validate_${this._getEditorId()}`, data); + + if (data.valid) { + data.promises.push(Promise.resolve()); + } else { + data.promises.push(Promise.reject()); + } + + return Promise.all(data.promises); + } + + /** + * Throws an error by showing an inline error for the target element. + */ + throwError(element: HTMLElement, message: string): void { + DomUtil.innerError(element, message); + } + + /** + * Shows the update message. + */ + protected _showMessage(data: AjaxResponseMessage): void { + const activeElement = this._activeElement!; + const editorId = this._getEditorId(); + const elementData = this._elements.get(activeElement)!; + + // set new content + DomUtil.setInnerHtml(elementData.messageBody.querySelector(".messageText")!, data.returnValues.message); + + // handle attachment list + if (typeof data.returnValues.attachmentList === "string") { + elementData.messageFooter + .querySelectorAll(".attachmentThumbnailList, .attachmentFileList") + .forEach((el) => el.remove()); + + const element = document.createElement("div"); + DomUtil.setInnerHtml(element, data.returnValues.attachmentList); + + let node; + while (element.childNodes.length) { + node = element.childNodes[element.childNodes.length - 1]; + elementData.messageFooter.insertBefore(node, elementData.messageFooter.firstChild); + } + } + + if (typeof data.returnValues.poll === "string") { + const poll = elementData.messageBody.querySelector(".pollContainer"); + if (poll !== null) { + // The poll container is wrapped inside `.jsInlineEditorHideContent`. + poll.parentElement!.remove(); + } + + const pollContainer = document.createElement("div"); + pollContainer.className = "jsInlineEditorHideContent"; + DomUtil.setInnerHtml(pollContainer, data.returnValues.poll); + + elementData.messageBody.insertAdjacentElement("afterbegin", pollContainer); + } + + this._restoreMessage(); + + this._updateHistory(this._getHash(this._getObjectId(activeElement))); + + EventHandler.fire("com.woltlab.wcf.redactor", `autosaveDestroy_${editorId}`); + + UiNotification.show(); + + if (this._options.quoteManager) { + this._options.quoteManager.clearAlternativeEditor(); + this._options.quoteManager.countQuotes(); + } + } + + /** + * Hides the editor from view. + */ + protected _hideEditor(): void { + const elementData = this._elements.get(this._activeElement!)!; + const editorContainer = elementData.messageBodyEditor!.querySelector(".editorContainer") as HTMLElement; + DomUtil.hide(editorContainer); + + const icon = document.createElement("span"); + icon.className = "icon icon48 fa-spinner"; + elementData.messageBodyEditor!.appendChild(icon); + } + + /** + * Restores the previously hidden editor. + */ + protected _restoreEditor(): void { + const elementData = this._elements.get(this._activeElement!)!; + const messageBodyEditor = elementData.messageBodyEditor!; + + const icon = messageBodyEditor.querySelector(".fa-spinner") as HTMLElement; + icon.remove(); + + const editorContainer = messageBodyEditor.querySelector(".editorContainer") as HTMLElement; + if (editorContainer !== null) { + DomUtil.show(editorContainer); + } + } + + /** + * Destroys the editor instance. + */ + protected _destroyEditor(): void { + EventHandler.fire("com.woltlab.wcf.redactor2", `autosaveDestroy_${this._getEditorId()}`); + EventHandler.fire("com.woltlab.wcf.redactor2", `destroy_${this._getEditorId()}`); + } + + /** + * Returns the hash added to the url after successfully editing a message. + */ + protected _getHash(objectId: number): string { + return `#message${objectId}`; + } + + /** + * Updates the history to avoid old content when going back in the browser + * history. + */ + protected _updateHistory(hash: string): void { + window.location.hash = hash; + } + + /** + * Returns the unique editor id. + */ + protected _getEditorId(): string { + return this._options.editorPrefix + this._getObjectId(this._activeElement!).toString(); + } + + /** + * Returns the element's `data-object-id` value. + */ + protected _getObjectId(element: HTMLElement): number { + return ~~(element.dataset.objectId || ""); + } + + _ajaxFailure(data: ResponseData): boolean { + const elementData = this._elements.get(this._activeElement!)!; + const editor = elementData.messageBodyEditor!.querySelector(".redactor-layer") as HTMLElement; + + // handle errors occurring on editor load + if (editor === null) { + this._restoreMessage(); + + return true; + } + + this._restoreEditor(); + + if (!data || data.returnValues === undefined || data.returnValues.realErrorMessage === undefined) { + return true; + } + + DomUtil.innerError(editor, data.returnValues.realErrorMessage); + + return false; + } + + _ajaxSuccess(data: ResponseData): void { + switch (data.actionName) { + case "beginEdit": + this._showEditor(data as AjaxResponseEditor); + break; + + case "save": + this._showMessage(data as AjaxResponseMessage); + break; + } + } + + _ajaxSetup(): ReturnType { + return { + data: { + className: this._options.className, + interfaceName: "wcf\\data\\IMessageInlineEditorAction", + }, + silent: true, + }; + } + + /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */ + legacyEdit(containerId: string): void { + this._click(document.getElementById(containerId), null); + } +} + +Core.enableLegacyInheritance(UiMessageInlineEditor); + +export = UiMessageInlineEditor; -- 2.20.1