Simplified API for notifications based on promises ui-notification-ergonomics
authorAlexander Ebert <ebert@woltlab.com>
Wed, 4 May 2022 14:52:06 +0000 (16:52 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 4 May 2022 14:52:06 +0000 (16:52 +0200)
The old API permitted arbitrary CSS class name that aren't guaranteed to work. Typical usage includes the default class names only therefore we can clamp the value to those.

Using promises cleans up the code flow when a “callback” is used.

ts/WoltLabSuite/Core/Ui/Notification.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Notification.js

index 47d7f592cab7c0dd8c5b666fcc94970c7baa94f7..425bb96bc923976caaa9e15bc1d460b2dc69c301 100644 (file)
@@ -1,35 +1,39 @@
 /**
  * Simple notification overlay.
  *
- * @author  Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module  Ui/Notification (alias)
- * @module  WoltLabSuite/Core/Ui/Notification
+ * @author Alexander Ebert
+ * @copyright 2001-2022 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module Ui/Notification (alias)
+ * @module WoltLabSuite/Core/Ui/Notification
  */
 
 import * as Language from "../Language";
 
 type Callback = () => void;
 
-let _busy = false;
-let _callback: Callback | null = null;
-let _didInit = false;
+const enum NotificationType {
+  Error = "error",
+  Info = "info",
+  Success = "success",
+  Warning = "warning",
+}
+
 let _message: HTMLElement;
 let _notificationElement: HTMLElement;
+let _pendingNotification: Promise<void> | undefined = undefined;
 let _timeout: number;
 
 function init() {
-  if (_didInit) {
+  if (_notificationElement instanceof HTMLElement) {
     return;
   }
-  _didInit = true;
 
   _notificationElement = document.createElement("div");
   _notificationElement.id = "systemNotification";
 
   _message = document.createElement("p");
-  _message.addEventListener("click", hide);
+  _message.addEventListener("click", () => hideNotification());
   _notificationElement.appendChild(_message);
 
   document.body.appendChild(_notificationElement);
@@ -38,33 +42,68 @@ function init() {
 /**
  * Hides the notification and invokes the callback if provided.
  */
-function hide() {
-  clearTimeout(_timeout);
+function hideNotification() {
+  window.clearTimeout(_timeout);
 
   _notificationElement.classList.remove("active");
+}
+
+async function notify(type: NotificationType, message: string): Promise<void> {
+  init();
 
-  if (_callback !== null) {
-    _callback();
+  if (_notificationElement.classList.contains("active")) {
+    await _pendingNotification!;
   }
 
-  _busy = false;
+  _message.className = type;
+  _message.textContent = Language.get(message);
+
+  _notificationElement.classList.add("active");
+
+  _pendingNotification = new Promise<void>((resolve) => {
+    _timeout = window.setTimeout(() => {
+      resolve();
+    }, 2_000);
+  }).then(() => hideNotification());
+
+  return _pendingNotification;
 }
 
 /**
- * Displays a notification.
+ * @deprecated 5.5 Use `notifyError()`, `notifyInfo()`, `notifySuccess()` or `notifyWarning()` instead
  */
-export function show(message?: string, callback?: Callback | null, cssClassName?: string): void {
-  if (_busy) {
-    return;
+export function show(message = "", callback: Callback | null = null, cssClassName = ""): void {
+  const validTypes = [
+    NotificationType.Error,
+    NotificationType.Info,
+    NotificationType.Success,
+    NotificationType.Warning,
+  ];
+
+  if (!validTypes.includes(cssClassName as NotificationType)) {
+    cssClassName = NotificationType.Success;
   }
-  _busy = true;
 
-  init();
+  const notification = notify(cssClassName as NotificationType, message);
+  void notification.then(() => {
+    if (typeof callback === "function") {
+      callback();
+    }
+  });
+}
 
-  _callback = typeof callback === "function" ? callback : null;
-  _message.className = cssClassName || "success";
-  _message.textContent = Language.get(message || "wcf.global.success");
+export async function notifyError(message = ""): Promise<void> {
+  return notify(NotificationType.Error, message);
+}
 
-  _notificationElement.classList.add("active");
-  _timeout = setTimeout(hide, 2000);
+export async function notifyInfo(message = ""): Promise<void> {
+  return notify(NotificationType.Info, message);
+}
+
+export async function notifySuccess(message = ""): Promise<void> {
+  return notify(NotificationType.Success, message);
+}
+
+export async function notifyWarning(message = ""): Promise<void> {
+  return notify(NotificationType.Warning, message);
 }
index 3b5d89d471879a4ae0bf75bdd61167618fa86010..3033f2df52d86cd205fb7508c3622b79809a4d30 100644 (file)
@@ -1,60 +1,89 @@
 /**
  * Simple notification overlay.
  *
- * @author  Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module  Ui/Notification (alias)
- * @module  WoltLabSuite/Core/Ui/Notification
+ * @author Alexander Ebert
+ * @copyright 2001-2022 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module Ui/Notification (alias)
+ * @module WoltLabSuite/Core/Ui/Notification
  */
 define(["require", "exports", "tslib", "../Language"], function (require, exports, tslib_1, Language) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
-    exports.show = void 0;
+    exports.notifyWarning = exports.notifySuccess = exports.notifyInfo = exports.notifyError = exports.show = void 0;
     Language = tslib_1.__importStar(Language);
-    let _busy = false;
-    let _callback = null;
-    let _didInit = false;
     let _message;
     let _notificationElement;
+    let _pendingNotification = undefined;
     let _timeout;
     function init() {
-        if (_didInit) {
+        if (_notificationElement instanceof HTMLElement) {
             return;
         }
-        _didInit = true;
         _notificationElement = document.createElement("div");
         _notificationElement.id = "systemNotification";
         _message = document.createElement("p");
-        _message.addEventListener("click", hide);
+        _message.addEventListener("click", () => hideNotification());
         _notificationElement.appendChild(_message);
         document.body.appendChild(_notificationElement);
     }
     /**
      * Hides the notification and invokes the callback if provided.
      */
-    function hide() {
-        clearTimeout(_timeout);
+    function hideNotification() {
+        window.clearTimeout(_timeout);
         _notificationElement.classList.remove("active");
-        if (_callback !== null) {
-            _callback();
+    }
+    async function notify(type, message) {
+        init();
+        if (_notificationElement.classList.contains("active")) {
+            await _pendingNotification;
         }
-        _busy = false;
+        _message.className = type;
+        _message.textContent = Language.get(message);
+        _notificationElement.classList.add("active");
+        _pendingNotification = new Promise((resolve) => {
+            _timeout = window.setTimeout(() => {
+                resolve();
+            }, 2000);
+        }).then(() => hideNotification());
+        return _pendingNotification;
     }
     /**
-     * Displays a notification.
+     * @deprecated 5.5 Use `notifyError()`, `notifyInfo()`, `notifySuccess()` or `notifyWarning()` instead
      */
-    function show(message, callback, cssClassName) {
-        if (_busy) {
-            return;
+    function show(message = "", callback = null, cssClassName = "") {
+        const validTypes = [
+            "error" /* Error */,
+            "info" /* Info */,
+            "success" /* Success */,
+            "warning" /* Warning */,
+        ];
+        if (!validTypes.includes(cssClassName)) {
+            cssClassName = "success" /* Success */;
         }
-        _busy = true;
-        init();
-        _callback = typeof callback === "function" ? callback : null;
-        _message.className = cssClassName || "success";
-        _message.textContent = Language.get(message || "wcf.global.success");
-        _notificationElement.classList.add("active");
-        _timeout = setTimeout(hide, 2000);
+        const notification = notify(cssClassName, message);
+        void notification.then(() => {
+            if (typeof callback === "function") {
+                callback();
+            }
+        });
     }
     exports.show = show;
+    async function notifyError(message = "") {
+        return notify("error" /* Error */, message);
+    }
+    exports.notifyError = notifyError;
+    async function notifyInfo(message = "") {
+        return notify("info" /* Info */, message);
+    }
+    exports.notifyInfo = notifyInfo;
+    async function notifySuccess(message = "") {
+        return notify("success" /* Success */, message);
+    }
+    exports.notifySuccess = notifySuccess;
+    async function notifyWarning(message = "") {
+        return notify("warning" /* Warning */, message);
+    }
+    exports.notifyWarning = notifyWarning;
 });