2 * Provides access and editing of message properties.
4 * @author Alexander Ebert
5 * @copyright 2001-2019 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 * @module WoltLabSuite/Core/Ui/Message/Manager
10 import * as Ajax from "../../Ajax";
11 import { AjaxCallbackObject, AjaxCallbackSetup, ResponseData } from "../../Ajax/Data";
12 import * as Core from "../../Core";
13 import DomChangeListener from "../../Dom/Change/Listener";
14 import * as Language from "../../Language";
15 import * as StringUtil from "../../StringUtil";
17 interface MessageManagerOptions {
22 type StringableValue = boolean | number | string;
24 class UiMessageManager implements AjaxCallbackObject {
25 protected readonly _elements = new Map<string, HTMLElement>();
26 protected readonly _options: MessageManagerOptions;
29 * Initializes a new manager instance.
31 constructor(options: MessageManagerOptions) {
32 this._options = Core.extend(
38 ) as MessageManagerOptions;
42 DomChangeListener.add(`Ui/Message/Manager${this._options.className}`, this.rebuild.bind(this));
46 * Rebuilds the list of observed messages. You should call this method whenever a
47 * message has been either added or removed from the document.
50 this._elements.clear();
52 document.querySelectorAll(this._options.selector).forEach((element: HTMLElement) => {
53 this._elements.set(element.dataset.objectId!, element);
58 * Returns a boolean value for the given permission. The permission should not start
59 * with "can" or "can-" as this is automatically assumed by this method.
61 getPermission(objectId: string, permission: string): boolean {
62 permission = "can" + StringUtil.ucfirst(permission);
63 const element = this._elements.get(objectId);
64 if (element === undefined) {
65 throw new Error(`Unknown object id '${objectId}' for selector '${this._options.selector}'`);
68 return Core.stringToBool(element.dataset[permission] || "");
72 * Returns the given property value from a message, optionally supporting a boolean return value.
74 getPropertyValue(objectId: string, propertyName: string, asBool: true): boolean;
75 getPropertyValue(objectId: string, propertyName: string, asBool: false): string;
76 getPropertyValue(objectId: string, propertyName: string, asBool: boolean): boolean | string {
77 const element = this._elements.get(objectId);
78 if (element === undefined) {
79 throw new Error(`Unknown object id '${objectId}' for selector '${this._options.selector}'`);
82 const value = element.dataset[StringUtil.toCamelCase(propertyName)] || "";
85 return Core.stringToBool(value);
92 * Invokes a method for given message object id in order to alter its state or properties.
94 update(objectId: string, actionName: string, parameters?: ArbitraryObject): void {
96 actionName: actionName,
97 parameters: parameters || {},
98 objectIDs: [objectId],
103 * Updates properties and states for given object ids. Keep in mind that this method does
104 * not support setting individual properties per message, instead all property changes
105 * are applied to all matching message objects.
107 updateItems(objectIds: string | string[], data: ArbitraryObject): void {
108 if (!Array.isArray(objectIds)) {
109 objectIds = [objectIds];
112 objectIds.forEach((objectId) => {
113 const element = this._elements.get(objectId);
114 if (element === undefined) {
118 Object.entries(data).forEach(([key, value]) => {
119 this._update(element, key, value as StringableValue);
125 * Bulk updates the properties and states for all observed messages at once.
127 updateAllItems(data: ArbitraryObject): void {
128 const objectIds = Array.from(this._elements.keys());
130 this.updateItems(objectIds, data);
134 * Sets or removes a message note identified by its unique CSS class.
136 setNote(objectId: string, className: string, htmlContent: string): void {
137 const element = this._elements.get(objectId);
138 if (element === undefined) {
139 throw new Error(`Unknown object id '${objectId}' for selector '${this._options.selector}'`);
142 const messageFooterNotes = element.querySelector(".messageFooterNotes") as HTMLElement;
143 let note = messageFooterNotes.querySelector(`.${className}`);
146 note = document.createElement("p");
147 note.className = "messageFooterNote " + className;
149 messageFooterNotes.appendChild(note);
152 note.innerHTML = htmlContent;
153 } else if (note !== null) {
159 * Updates a single property of a message element.
161 protected _update(element: HTMLElement, propertyName: string, propertyValue: StringableValue): void {
162 element.dataset[propertyName] = propertyValue.toString();
164 // handle special properties
165 const propertyValueBoolean = propertyValue == 1 || propertyValue === true || propertyValue === "true";
166 this._updateState(element, propertyName, propertyValue, propertyValueBoolean);
170 * Updates the message element's state based upon a property change.
172 protected _updateState(
173 element: HTMLElement,
174 propertyName: string,
175 propertyValue: StringableValue,
176 propertyValueBoolean: boolean,
178 switch (propertyName) {
180 if (propertyValueBoolean) {
181 element.classList.add("messageDeleted");
183 element.classList.remove("messageDeleted");
186 this._toggleMessageStatus(element, "jsIconDeleted", "wcf.message.status.deleted", "red", propertyValueBoolean);
191 if (propertyValueBoolean) {
192 element.classList.add("messageDisabled");
194 element.classList.remove("messageDisabled");
197 this._toggleMessageStatus(
200 "wcf.message.status.disabled",
202 propertyValueBoolean,
210 * Toggles the message status bade for provided element.
212 protected _toggleMessageStatus(
213 element: HTMLElement,
219 let messageStatus = element.querySelector(".messageStatus");
220 if (messageStatus === null) {
221 const messageHeaderMetaData = element.querySelector(".messageHeaderMetaData");
222 if (messageHeaderMetaData === null) {
223 // can't find appropriate location to insert badge
227 messageStatus = document.createElement("ul");
228 messageStatus.className = "messageStatus";
229 messageHeaderMetaData.insertAdjacentElement("afterend", messageStatus);
232 let badge = messageStatus.querySelector(`.${className}`);
234 if (badge !== null) {
235 // badge already exists
239 badge = document.createElement("span");
240 badge.className = `badge label ${badgeColor} ${className}`;
241 badge.textContent = Language.get(phrase);
243 const listItem = document.createElement("li");
244 listItem.appendChild(badge);
245 messageStatus.appendChild(listItem);
247 if (badge === null) {
248 // badge does not exist
252 badge.parentElement!.remove();
257 * Transforms camel-cased property names into their attribute equivalent.
259 * @deprecated 5.4 Access the value via `element.dataset` which uses camel-case.
261 protected _getAttributeName(propertyName: string): string {
262 if (propertyName.indexOf("-") !== -1) {
267 .split(/([A-Z][a-z]+)/)
268 .map((s) => s.trim().toLowerCase())
269 .filter((s) => s.length > 0)
273 _ajaxSuccess(_data: ResponseData): void {
274 // This should be an abstract method, but cannot be marked as such for backwards compatibility.
275 throw new Error("Method _ajaxSuccess() must be implemented by deriving functions.");
278 _ajaxSetup(): ReturnType<AjaxCallbackSetup> {
281 className: this._options.className,
287 Core.enableLegacyInheritance(UiMessageManager);
289 export = UiMessageManager;