From: Alexander Ebert Date: Mon, 16 Nov 2015 23:53:23 +0000 (+0100) Subject: Added generic message manager X-Git-Tag: 3.0.0_Beta_1~2030^2~248 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=1a86aecd14f39a394df01f787aaf2e0dd50213df;p=GitHub%2FWoltLab%2FWCF.git Added generic message manager --- diff --git a/wcfsetup/install/files/js/WCF.Message.js b/wcfsetup/install/files/js/WCF.Message.js index 3e581f5276..4d3506d05d 100644 --- a/wcfsetup/install/files/js/WCF.Message.js +++ b/wcfsetup/install/files/js/WCF.Message.js @@ -1224,7 +1224,7 @@ WCF.Message.InlineEditor = Class.extend({ /** * container id - * @var integer + * @var int */ _containerID: 0, @@ -1264,8 +1264,7 @@ WCF.Message.InlineEditor = Class.extend({ messageSelector: this._messageContainerSelector, - callbackDropdownInit: this._callbackDropdownInit.bind(this), - callbackLegacyInitElements: this._callbackInitElements.bind(this) + callbackDropdownInit: this._callbackDropdownInit.bind(this) }); }).bind(this)); }, diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js index 1a00647608..f6be43116c 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js @@ -10,43 +10,40 @@ define( [ 'Ajax', 'Core', 'Dictionary', 'Environment', 'EventHandler', 'Language', 'ObjectMap', 'Dom/Traverse', - 'Dom/Util', 'Ui/Notification', 'Ui/SimpleDropdown' + 'Dom/Util', 'Ui/Notification', 'Ui/ReusableDropdown' ], function( Ajax, Core, Dictionary, Environment, EventHandler, Language, ObjectMap, DomTraverse, - DomUtil, UiNotification, UiSimpleDropdown + DomUtil, UiNotification, UiReusableDropdown ) { "use strict"; - var _activeElement = null; - var _dropdownMenus = new Dictionary(); - var _elements = new ObjectMap(); - var _options = {}; - /** - * @exports WoltLab/WCF/Ui/Message/InlineEditor + * @constructor */ - var UiMessageInlineEditor = { + function UiMessageInlineEditor(options) { this.init(options); } + UiMessageInlineEditor.prototype = { /** * Initializes the message inline editor. * - * @param {object} options list of configuration options + * @param {Object} options list of configuration options */ init: function(options) { - _options = Core.extend({ + this._activeElement = null; + this._dropdownMenu = null; + this._elements = new ObjectMap(); + this._options = Core.extend({ canEditInline: false, extendedForm: true, className: '', containerId: 0, + dropdownIdentifier: '', editorPrefix: 'messageEditor', - messageSelector: '.jsMessage', - - callbackDropdownInit: null, - callbackDropdownOpen: null + messageSelector: '.jsMessage' }, options); this._initElements(); @@ -54,21 +51,25 @@ define( /** * Initializes each applicable message. + * + * @protected */ _initElements: function() { - var button, canEdit, element, elements = elBySelAll(_options.messageSelector); + var button, canEdit, element, elements = elBySelAll(this._options.messageSelector); + for (var i = 0, length = elements.length; i < length; i++) { element = elements[i]; - if (_elements.has(element)) { + if (this._elements.has(element)) { continue; } button = elBySel('.jsMessageEditButton', element); if (button !== null) { - canEdit = elAttrBool(element, 'data-can-edit'); + canEdit = elDataBool(element, 'can-edit'); - if (_options.canEditInline) { + if (this._options.canEditInline) { button.addEventListener('click', this._clickDropdown.bind(this, element)); + button.classList.add('jsDropdownEnabled'); if (canEdit) { button.addEventListener('dblclick', this._click.bind(this, element)); @@ -79,11 +80,11 @@ define( } } - var messageBody = elBySel('.messageBody', element); var messageFooter = elBySel('.messageFooter', element); - _elements.set(element, { + this._elements.set(element, { + button: button, messageBody: messageBody, messageBodyEditor: null, messageFooter: messageFooter, @@ -97,20 +98,21 @@ define( * Handles clicks on the edit button or the edit dropdown item. * * @param {Element} element message element - * @param {?object} event event object + * @param {?Event} event event object + * @protected */ _click: function(element, event) { if (event !== null) event.preventDefault(); - if (_activeElement === null) { - _activeElement = element; + if (this._activeElement === null) { + this._activeElement = element; this._prepare(); Ajax.api(this, { actionName: 'beginEdit', parameters: { - containerID: _options.containerId, + containerID: this._options.containerId, objectID: this._getObjectId(element) } }); @@ -124,7 +126,8 @@ define( * Creates and opens the dropdown on first usage. * * @param {Element} element message element - * @param {object} event event object + * @param {Object} event event object + * @protected */ _clickDropdown: function(element, event) { event.preventDefault(); @@ -134,98 +137,169 @@ define( return; } - // build dropdown button.classList.add('dropdownToggle'); button.parentNode.classList.add('dropdown'); + (function(button, element) { + button.addEventListener('click', (function(event) { + event.preventDefault(); + event.stopPropagation(); + + this._activeElement = element; + UiReusableDropdown.toggleDropdown(this._options.dropdownIdentifier, button); + }).bind(this)); + }).bind(this)(button, element); - var dropdownMenu = elCreate('ul'); - dropdownMenu.className = 'dropdownMenu'; - - var items = _options.callbackDropdownInit(element, dropdownMenu); - if (items !== null) this._dropdownBuild(element, dropdownMenu, items); - - DomUtil.insertAfter(dropdownMenu, button); - - _dropdownMenus.set(this._getObjectId(element), dropdownMenu); - - UiSimpleDropdown.init(button, true); + // 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)); + } - var id = DomUtil.identify(button.parentNode); - UiSimpleDropdown.registerCallback(id, this._dropdownToggle.bind(this, element)); + setTimeout(function() { + Core.triggerEvent(button, 'click'); + }, 10); }, /** * Creates the dropdown menu on first usage. * - * @param {Element} element message element - * @param {Element} dropdownMenu dropdown menu - * @param {array} items list of dropdown items + * @param {Object} items list of dropdown items + * @protected */ - _dropdownBuild: function(element, dropdownMenu, items) { + _dropdownBuild: function(items) { var item, label, listItem; - var callbackClick = this._clickDropdownItem.bind(this, element); + 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.special === 'divider') { + if (item.item === 'divider') { listItem.className = 'dropdownDivider'; } else { - elData(listItem, 'action', item.action); label = elCreate('span'); label.textContent = Language.get(item.label); listItem.appendChild(label); - if (item.special === 'edit') { - listItem.addEventListener('click', this._click.bind(this, element)); + if (item.action === 'editItem') { + listItem.addEventListener('click', this._click.bind(this)); } else { listItem.addEventListener('click', callbackClick); } - - if (item.visible === false) { - elHide(listItem); - } } - dropdownMenu.appendChild(listItem); + this._dropdownMenu.appendChild(listItem); } }, /** * Callback for dropdown toggle. * - * @param {Element} element message element - * @param {integer} containerId container id + * @param {int} containerId container id * @param {string} action toggle action, either 'open' or 'close' + * @protected */ - _dropdownToggle: function(element, containerId, action) { - _elements.get(element).messageFooterButtons.classList[(action === 'open' ? 'add' : 'remove')]('forceVisible'); + _dropdownToggle: function(containerId, action) { + var elementData = this._elements.get(this._activeElement); + elementData.button.parentNode.classList[(action === 'open' ? 'add' : 'remove')]('dropdownOpen'); + elementData.messageFooterButtons.classList[(action === 'open' ? 'add' : 'remove')]('forceVisible'); - if (action === 'open' && typeof _options.callbackDropdownOpen === 'function') { - _options.callbackDropdownOpen(element, this._getObjectId(element)); + if (action === 'open') { + var visibility = this._dropdownOpen(); + + EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownOpen_' + this._options.dropdownIdentifier, { + element: this._activeElement, + 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 (visibility.hasOwnProperty(item) && visibility[item] === false) { + elHide(listItem); + } + 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 {Element} element message element - * @param {object} event event object + * @param {Event} event event object + * @protected */ - _clickDropdownItem: function(element, event) { + _clickDropdownItem: function(event) { event.preventDefault(); - _options.callbackDropdownSelect(element, this._getObjectId(element), elAttr(event.currentTarget, 'data-class-name')); + this._dropdownSelect(elData(event.currentTarget, 'item')); }, /** * Prepares the message for editor display. + * + * @protected */ _prepare: function() { - var data = _elements.get(_activeElement); + var data = this._elements.get(this._activeElement); var messageBodyEditor = elCreate('div'); messageBodyEditor.className = 'messageBody editor'; @@ -243,13 +317,14 @@ define( /** * Shows the message editor. * - * @param {object} data ajax response data + * @param {Object} data ajax response data + * @protected */ _showEditor: function(data) { var id = this._getEditorId(); - var elementData = _elements.get(_activeElement); + var elementData = this._elements.get(this._activeElement); - _activeElement.classList.add('jsInvalidQuoteTarget'); + this._activeElement.classList.add('jsInvalidQuoteTarget'); var icon = DomTraverse.childByClass(elementData.messageBodyEditor, 'icon'); icon.parentNode.removeChild(icon); @@ -265,7 +340,7 @@ define( var buttonSave = elBySel('button[data-type="save"]', formSubmit); buttonSave.addEventListener('click', this._save.bind(this)); - if (_options.extendedForm) { + if (this._options.extendedForm) { var buttonExtended = elBySel('button[data-type="extended"]', formSubmit); buttonExtended.addEventListener('click', this._prepareExtended.bind(this)); } @@ -291,7 +366,7 @@ define( } // TODO - new WCF.Effect.Scroll().scrollTo(_activeElement, true); + new WCF.Effect.Scroll().scrollTo(this._activeElement, true); }).bind(this), 250); } else { @@ -301,9 +376,11 @@ define( /** * Restores the message view. + * + * @protected */ _restoreMessage: function() { - var elementData = _elements.get(_activeElement); + var elementData = this._elements.get(this._activeElement); this._destroyEditor(); @@ -312,9 +389,9 @@ define( elShow(elementData.messageBody); elShow(elementData.messageFooter); - _activeElement.classList.remove('jsInvalidQuoteTarget'); + this._activeElement.classList.remove('jsInvalidQuoteTarget'); - _activeElement = null; + this._activeElement = null; // @TODO if (this._quoteManager) { @@ -324,10 +401,12 @@ define( /** * Saves the editor message. + * + * @protected */ _save: function() { var parameters = { - containerID: _options.containerId, + containerID: this._options.containerId, data: { message: '' }, @@ -350,10 +429,11 @@ define( /** * Shows the update message. * - * @param {object} data ajax response data + * @param {Object} data ajax response data + * @protected */ _showMessage: function(data) { - var elementData = _elements.get(_activeElement); + var elementData = this._elements.get(this._activeElement); var attachmentLists = elBySelAll('.attachmentThumbnailList, .attachmentFileList', elementData.messageBody); // set new content @@ -390,12 +470,14 @@ define( /** * Initiates the jump to the extended edit form. + * + * @protected */ _prepareExtended: function() { var data = { actionName: 'jumpToExtended', parameters: { - containerID: _options.containerId, + containerID: this._options.containerId, message: '', messageID: this._getObjectId() } @@ -409,9 +491,11 @@ define( /** * Hides the editor from view. + * + * @protected */ _hideEditor: function() { - var elementData = _elements.get(_activeElement); + var elementData = this._elements.get(this._activeElement); elHide(DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer')); var icon = elCreate('span'); @@ -421,9 +505,11 @@ define( /** * Restores the previously hidden editor. + * + * @protected */ _restoreEditor: function() { - var elementData = _elements.get(_activeElement); + var elementData = this._elements.get(this._activeElement); var icon = elBySel('.fa-spinner', elementData.messageBodyEditor); elRemove(icon); console.debug(icon); @@ -432,6 +518,8 @@ define( /** * Destroys the editor instance. + * + * @protected */ _destroyEditor: function() { EventHandler.fire('com.woltlab.wcf.redactor', 'destroy_' + this._getEditorId()); @@ -440,8 +528,9 @@ define( /** * Returns the hash added to the url after successfully editing a message. * - * @param {integer} objectId message object id + * @param {int} objectId message object id * @return string + * @protected */ _getHash: function(objectId) { return '#message' + objectId; @@ -451,7 +540,8 @@ define( * Updates the history to avoid old content when going back in the browser * history. * - * @param hash + * @param {string} hash location hash + * @protected */ _updateHistory: function(hash) { window.location.hash = hash; @@ -461,19 +551,21 @@ define( * Returns the unique editor id. * * @return {string} editor id + * @protected */ _getEditorId: function() { - return _options.editorPrefix + this._getObjectId(); + return this._options.editorPrefix + this._getObjectId(); }, /** * Returns the element's `data-object-id` value. * - * @param {Element=} element target element, `_activeElement` if empty - * @return {integer} + * @param {Element=} element target element, `this._activeElement` if empty + * @return {int} + * @protected */ _getObjectId: function(element) { - return ~~elAttr(element || _activeElement, 'data-object-id'); + return ~~elData(element || this._activeElement, 'object-id'); }, _ajaxFailure: function(data) { @@ -483,7 +575,7 @@ define( return true; } - var elementData = _elements.get(_activeElement); + var elementData = this._elements.get(this._activeElement); var innerError = elBySel('.innerError', elementData.messageBodyEditor); if (innerError === null) { innerError = elCreate('small'); @@ -518,17 +610,17 @@ define( _ajaxSetup: function() { return { data: { - className: _options.className, + className: this._options.className, interfaceName: 'wcf\\data\\IMessageInlineEditorAction' } }; }, /** @deprecated 2.2 - used only for backward compatibility with `WCF.Message.InlineEditor` */ - legacyGetDropdownMenus: function() { return _dropdownMenus; }, + legacyGetDropdownMenus: function() { return this._dropdownMenus; }, /** @deprecated 2.2 - used only for backward compatibility with `WCF.Message.InlineEditor` */ - legacyGetElements: function() { return _elements; }, + legacyGetElements: function() { return this._elements; }, /** @deprecated 2.2 - used only for backward compatibility with `WCF.Message.InlineEditor` */ legacyEdit: function(containerId) { diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Manager.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Manager.js new file mode 100644 index 0000000000..1799e39cb1 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Manager.js @@ -0,0 +1,216 @@ +/** + * Provides access and editing of message properties. + * + * @author Alexander Ebert + * @copyright 2001-2015 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLab/WCF/Ui/Message/Manager + */ +define(['Ajax', 'Core', 'Dictionary'], function(Ajax, Core, Dictionary) { + "use strict"; + + /** + * @param {Object} options initilization options + * @constructor + */ + function UiMessageManager(options) { this.init(options); } + UiMessageManager.prototype = { + /** + * Initializes a new manager instance. + * + * @param {Object} options initilization options + */ + init: function(options) { + this._elements = null; + this._options = Core.extend({ + className: '', + selector: '' + }, options); + + this.rebuild(); + }, + + /** + * Rebuilds the list of observed messages. You should call this method whenever a + * message has been either added or removed from the document. + */ + rebuild: function() { + this._elements = new Dictionary(); + + var element, elements = elBySelAll(this._options.selector); + for (var i = 0, length = elements.length; i < length; i++) { + element = elements[i]; + + this._elements.set(elData(element, 'object-id'), element); + } + }, + + /** + * Returns a boolean value for the given permission. The permission should not start + * with "can" or "can-" as this is automatically assumed by this method. + * + * @param {int} objectId message object id + * @param {string} permission permission name without a leading "can" or "can-" + * @return {boolean} true if permission was set and is either 'true' or '1' + */ + getPermission: function(objectId, permission) { + permission = 'can-' + this._getAttributeName(permission); + var element = this._elements.get(objectId); + if (element === undefined) { + throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'"); + } + + return elDataBool(element, permission); + }, + + /** + * Returns the given property value from a message, optionally supporting a boolean return value. + * + * @param {int} objectId message object id + * @param {string} propertyName attribute name + * @param {boolean} asBool attempt to interpret property value as boolean + * @return {(boolean|string)} raw property value or boolean if requested + */ + getPropertyValue: function(objectId, propertyName, asBool) { + var element = this._elements.get(objectId); + if (element === undefined) { + throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'"); + } + + return window[(asBool ? 'elDataBool' : 'elData')](element, this._getAttributeName(propertyName)); + }, + + /** + * Invokes a method for given message object id in order to alter its state or properties. + * + * @param {int} objectId message object id + * @param {string} actionName action name used for the ajax api + * @param {Object=} parameters optional list of parameters included with the ajax request + */ + update: function(objectId, actionName, parameters) { + Ajax.api(this, { + actionName: actionName, + parameters: parameters || {}, + objectIDs: [objectId] + }); + }, + + /** + * Updates properties and states for given object ids. Keep in mind that this method does + * not support setting individual properties per message, instead all property changes + * are applied to all matching message objects. + * + * @param {Array} objectIds list of message object ids + * @param {Object} data list of updated properties + */ + updateItems: function(objectIds, data) { + if (!Array.isArray(objectIds)) { + objectIds = [objectIds]; + } + + var element; + for (var i = 0, length = objectIds.length; i < length; i++) { + element = this._elements.get(objectIds[i]); + if (element === undefined) { + continue; + } + + for (var key in data) { + if (data.hasOwnProperty(key)) { + this._update(element, key, data[key]); + } + } + } + }, + + /** + * Bulk updates the properties and states for all observed messages at once. + * + * @param {Object} data list of updated properties + */ + updateAllItems: function(data) { + var objectIds = []; + this._elements.forEach((function(element, objectId) { + objectIds.push(objectId); + }).bind(this)); + + this.update(objectIds, data); + }, + + /** + * Updates a single property of a message element. + * + * @param {Element} element message element + * @param {string} propertyName property name + * @param {?} propertyValue property value, will be implicitly converted to string + * @protected + */ + _update: function(element, propertyName, propertyValue) { + elData(element, this._getAttributeName(propertyName), propertyValue); + + // handle special properties + var propertyValueBoolean = (propertyValue == 1 || propertyValue === true || propertyValue === 'true'); + this._updateState(element, propertyName, propertyValue, propertyValueBoolean); + }, + + /** + * Updates the message element's state based upon a property change. + * + * @param {Element} element message element + * @param {string} propertyName property name + * @param {?} propertyValue property value + * @param {boolean} propertyValueBoolean true if `propertyValue` equals either 'true' or '1' + * @protected + */ + _updateState: function(element, propertyName, propertyValue, propertyValueBoolean) { + switch (propertyName) { + case 'isDeleted': + element.classList[(propertyValueBoolean ? 'add' : 'remove')]('messageDeleted'); + break; + + case 'isDisabled': + element.classList[(propertyValueBoolean ? 'add' : 'remove')]('messageDisabled'); + break; + } + }, + + /** + * Transforms camel-cased property names into their attribute equivalent. + * + * @param {string} propertyName camel-cased property name + * @return {string} equivalent attribute name + * @protected + */ + _getAttributeName: function(propertyName) { + if (propertyName.indexOf('-') !== -1) { + return propertyName; + } + + var attributeName = ''; + var str, tmp = propertyName.split(/([A-Z][a-z]+)/); + for (var i = 0, length = tmp.length; i < length; i++) { + str = tmp[i]; + if (str.length) { + if (attributeName.length) attributeName += '-'; + attributeName += str.toLowerCase(); + } + } + + return attributeName; + }, + + _ajaxSuccess: function(data) { + throw new Error("Method _ajaxSuccess() must be implemented by deriving functions."); + }, + + _ajaxSetup: function() { + return { + data: { + className: this._options.className + } + }; + } + }; + + return UiMessageManager; +}); \ No newline at end of file