Răsfoiți Sursa

feat: add .once() method to NanoEmitter

Sven 1 an în urmă
părinte
comite
260080a01f
2 a modificat fișierele cu 68 adăugiri și 22 ștergeri
  1. 24 17
      src/components/BytmDialog.ts
  2. 44 5
      src/utils/NanoEmitter.ts

+ 24 - 17
src/components/BytmDialog.ts

@@ -50,7 +50,7 @@ export class BytmDialog extends NanoEmitter<{
   public readonly id;
 
   private dialogOpen = false;
-  private dialogRendered = false;
+  private dialogMounted = false;
   private listenersAttached = false;
 
   constructor(options: BytmDialogOptions) {
@@ -67,11 +67,13 @@ export class BytmDialog extends NanoEmitter<{
     this.id = options.id;
   }
 
+  //#MARKER public
+
   /** Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM */
   public async mount() {
-    if(this.dialogRendered)
+    if(this.dialogMounted)
       return;
-    this.dialogRendered = true;
+    this.dialogMounted = true;
 
     const bgElem = document.createElement("div");
     bgElem.id = `bytm-${this.id}-dialog-bg`;
@@ -100,7 +102,7 @@ export class BytmDialog extends NanoEmitter<{
 
   /** Clears all dialog contents (unmounts them from the DOM) in preparation for a new rendering call */
   public unmount() {
-    this.dialogRendered = false;
+    this.dialogMounted = false;
 
     const clearSelectors = [
       `#bytm-${this.id}-dialog-bg`,
@@ -116,8 +118,8 @@ export class BytmDialog extends NanoEmitter<{
     this.events.emit("clear");
   }
 
-  /** Clears and then re-renders the dialog */
-  public async rerender() {
+  /** Clears the DOM of the dialog and then renders it again */
+  public async remount() {
     this.unmount();
     await this.mount();
   }
@@ -134,7 +136,7 @@ export class BytmDialog extends NanoEmitter<{
       return;
     this.dialogOpen = true;
 
-    if(!this.isRendered())
+    if(!this.isMounted())
       await this.mount();
 
     document.body.classList.add("bytm-disable-scroll");
@@ -183,28 +185,23 @@ export class BytmDialog extends NanoEmitter<{
       this.destroy();
   }
 
-  /** Returns true if the dialog is open */
+  /** Returns true if the dialog is currently open */
   public isOpen() {
     return this.dialogOpen;
   }
 
-  /** Returns true if the dialog has been rendered */
-  public isRendered() {
-    return this.dialogRendered;
+  /** Returns true if the dialog is currently mounted */
+  public isMounted() {
+    return this.dialogMounted;
   }
 
-  /** Clears the dialog and removes all event listeners */
+  /** Clears the DOM of the dialog and removes all event listeners */
   public destroy() {
     this.events.emit("destroy");
     this.unmount();
     this.unsubscribeAll();
   }
 
-  /** Returns the ID of the top-most dialog (the dialog that has been opened last) */
-  public static getLastDialogId() {
-    return lastDialogId;
-  }
-
   /** Called once to attach all generic event listeners */
   public attachListeners(bgElem: HTMLElement) {
     if(this.listenersAttached)
@@ -226,6 +223,16 @@ export class BytmDialog extends NanoEmitter<{
     }
   }
 
+  //#MARKER static
+
+  /** Returns the ID of the top-most dialog (the dialog that has been opened last) */
+  public static getLastDialogId() {
+    return lastDialogId;
+  }
+
+  //#MARKER private
+
+  /** Returns the dialog content element and all its children */
   private async getDialogContent() {
     const header = this.options.renderHeader?.();
     const footer = this.options.renderFooter?.();

+ 44 - 5
src/utils/NanoEmitter.ts

@@ -1,9 +1,22 @@
 import { createNanoEvents, type DefaultEvents, type EventsMap, type Unsubscribe } from "nanoevents";
 
+interface NanoEmitterSettings {
+  /** If set to true, allows emitting events through the public method emit() */
+  publicEmit: boolean;
+}
+
 /** Abstract class that can be extended to create an event emitter with helper methods and a strongly typed event map */
 export abstract class NanoEmitter<TEvtMap extends EventsMap = DefaultEvents> {
   protected readonly events = createNanoEvents<TEvtMap>();
-  protected unsubscribers: Unsubscribe[] = [];
+  protected eventUnsubscribes: Unsubscribe[] = [];
+  protected emitterSettings: NanoEmitterSettings;
+
+  constructor(settings: Partial<NanoEmitterSettings> = {}) {
+    this.emitterSettings = {
+      publicEmit: false,
+      ...settings,
+    };
+  }
 
   /** Subscribes to an event - returns a function that unsubscribes the event listener */
   public on<TKey extends keyof TEvtMap>(event: TKey, cb: TEvtMap[TKey]) {
@@ -14,19 +27,45 @@ export abstract class NanoEmitter<TEvtMap extends EventsMap = DefaultEvents> {
       if(!unsub)
         return;
       unsub();
-      this.unsubscribers = this.unsubscribers.filter(u => u !== unsub);
+      this.eventUnsubscribes = this.eventUnsubscribes.filter(u => u !== unsub);
     };
 
     unsub = this.events.on(event, cb);
 
-    this.unsubscribers.push(unsub);
+    this.eventUnsubscribes.push(unsub);
     return unsubProxy;
   }
 
+  /** Subscribes to an event and calls the callback or resolves the Promise only once */
+  public once<TKey extends keyof TEvtMap>(event: TKey, cb?: TEvtMap[TKey]): Promise<Parameters<TEvtMap[TKey]>> {
+    return new Promise((resolve) => {
+      // eslint-disable-next-line prefer-const
+      let unsub: Unsubscribe | undefined;
+
+      const onceProxy = ((...args: Parameters<TEvtMap[TKey]>) => {
+        unsub?.();
+        cb?.(...args);
+        resolve(args);
+      }) as TEvtMap[TKey];
+
+      // eslint-disable-next-line prefer-const
+      unsub = this.on(event, onceProxy);
+    });
+  }
+
+  /** Emits an event on this instance - Needs `publicEmit` to be set to true in the constructor! */
+  public emit<TKey extends keyof TEvtMap>(event: TKey, ...args: Parameters<TEvtMap[TKey]>) {
+    if(this.emitterSettings.publicEmit) {
+      this.events.emit(event, ...args);
+      return true;
+    }
+    return false;
+  }
+
   /** Unsubscribes all event listeners */
   public unsubscribeAll() {
-    for(const unsub of this.unsubscribers)
+    for(const unsub of this.eventUnsubscribes)
       unsub();
-    this.unsubscribers = [];
+    this.eventUnsubscribes = [];
   }
 }