Ver código fonte

fix: expose BytmDialog and components on interface

Sv443 1 ano atrás
pai
commit
11d9bcd55f
3 arquivos alterados com 268 adições e 10 exclusões
  1. 252 0
      contributing.md
  2. 10 8
      src/components/BytmDialog.ts
  3. 6 2
      src/interface.ts

+ 252 - 0
contributing.md

@@ -720,5 +720,257 @@ The usage and example blocks on each are written in TypeScript but can be used i
 > ```
 > </details>
 
+<br>
+
+> #### NanoEmitter
+> Usage:  
+> ```ts
+> new unsafeWindow.BYTM.NanoEmitter<TEventMap>(settings: NanoEmitterSettings): NanoEmitter
+> ```
+>   
+> Abstract class that can be extended to create custom event emitting classes.  
+> The methods are fully typed through the generic `TEventMap`, which is an object map of event names to a callback function signature (the type is `Record<string, (...args: any) => void>`)  
+> 
+> <br>
+> 
+> Options properties:  
+> | Property | Description |
+> | :--- | :--- |
+> | `publicEmit?: boolean` | If set to true, allows emitting events through the public method emit() |
+> 
+> <br>
+> 
+> Methods:
+> - `on(event: string, callback: Function<any>): Function`  
+>   Registers a callback for the specified event.  
+>   Returns a function that can be called to unsubscribe from the event at any time.
+> - `once(event: string, callback?: Function<any>): Promise<any[]>`  
+>   Registers a callback for the specified event that gets called only once.  
+>   The callback is called and the Promise is resolved at the same time.
+> - `emit(event: string, ...args: any[]): boolean`  
+>   Emits the specified event with the passed arguments.  
+>   Has to be enabled through the `publicEmit` option in the constructor.  
+>   Returns true if the event was emitted successfully, false if not.
+> - `unsubscribeAll(): void`  
+>   Unsubscribes all listeners from all events and clears the internal listener map.
+> 
+> <details><summary><b>Example <i>(click to expand)</i></b></summary>
+> 
+> ```ts
+> interface MyEvents {
+>   /** Emitted when the foo is bar */
+>   foo: (bar: string) => void;
+> }
+> 
+> class MyEmitter extends unsafeWindow.BYTM.NanoEmitter<MyEvents> {
+>   constructor() {
+>     // allow calling emit() from outside this class instance
+>     super({ publicEmit: true });
+>   }
+> 
+>   public doSomething() {
+>     this.emit("foo", "baz");
+>   }
+> }
+> 
+> function run() {
+>   const emitter = new MyEmitter();
+> 
+>   emitter.on("foo", (bar) => {
+>     //                ^ automatically typed as string
+>     console.log(`The bar is ${bar}`);
+>   });
+> 
+>   // will log "The bar is baz" to the console, see above
+>   emitter.doSomething();
+> }
+> 
+> run();
+> ```
+> </details>
+
+<br>
+
+> #### BytmDialog
+> Usage:  
+> ```ts
+> new unsafeWindow.BYTM.BytmDialog(options: BytmDialogOptions): BytmDialog
+> ```
+>   
+> A class that can be used to create a dialog with a custom header, body and footer.  
+> The dialog is fully customizable and can be used to display any kind of content.  
+> It has many helper methods and events to make it flexible and easy to work with.  
+> The CSS style is uniform and can be overridden if a different theme is needed.  
+>   
+> Options properties:
+> | Property | Description |
+> | :--- | :--- |
+> | `id: string` | ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! |
+> | `maxWidth: number` | Maximum width of the dialog in pixels |
+> | `maxHeight: number` | Maximum height of the dialog in pixels |
+> | `closeOnBgClick?: boolean` | Whether the dialog should close when the background is clicked - defaults to true |
+> | `closeOnEscPress?: boolean` | Whether the dialog should close when the escape key is pressed - defaults to true |
+> | `closeBtnEnabled?: boolean` | Whether the close button should be enabled - defaults to true |
+> | `destroyOnClose?: boolean` | Whether the dialog should be destroyed when it's closed - defaults to false |
+> | `smallDialog?: boolean` | Whether the menu should have a smaller overall appearance - defaults to false |
+> | `renderBody: () => HTMLElement │ Promise<HTMLElement>` | Called to render the body of the dialog |
+> | `renderHeader?: () => HTMLElement │ Promise<HTMLElement>` | Called to render the header of the dialog - leave undefined for a blank header |
+> | `renderFooter?: () => HTMLElement │ Promise<HTMLElement>` | Called to render the footer of the dialog - leave undefined for no footer |
+> 
+> <br>
+> 
+> Methods:  
+> The methods from the [`NanoEmitter`](#nanoemitter) class are also available here.  
+> These are the additional methods that are exclusive to the `BytmDialog` class:  
+> - `open(e?: MouseEvent | KeyboardEvent): Promise<void>`  
+>   Opens the dialog - also mounts it if it hasn't been mounted yet.  
+>   Prevents default action and immediate propagation if an event is passed.  
+>   Resolves once the dialog is fully mounted and opened.
+> - `close(e?: MouseEvent | KeyboardEvent): void`  
+>   Closes the dialog - also unmounts and destroys it if the `destroyOnClose` option is set to true.  
+>   Prevents default action and immediate propagation if an event is passed.  
+> - `isOpen(): boolean`  
+>   Returns true if the dialog is currently open, false if not.
+> - `isMounted(): boolean`  
+>   Returns true if the dialog is currently mounted, false if not.
+> - `mount(): Promise<void>`  
+>   Mounts the dialog to the DOM without making it visible - can be called before opening the dialog for the first time to pre-load all elements.  
+>   Resolves once the dialog is fully mounted in the DOM.
+> - `unmount(): void`  
+>   Removes the dialog from the DOM.  
+> - `remount(): Promise<void>`  
+>   Unmounts and mounts the dialog again.  
+>   This can be used to re-render the dialog's contents with new information.  
+>   Resolves once the dialog is fully mounted in the DOM.
+> - `destroy(): void`  
+>   Unmounts and removes the dialog from the DOM and removes all event listeners.  
+>   Should be called when the dialog is no longer needed.
+> - `static getLastDialogId(): string`  
+>   Returns the ID of the last dialog that was opened.  
+>   This can be used to check if a dialog is currently open and to get its ID for further use.  
+>   Static method usage: `BytmDialog.getLastDialogId()`
+> 
+> <br>
+> 
+> Events:  
+> | Event | Description |
+> | :--- | :--- |
+> | on(`close`, () => void) | Called just after the dialog is closed |
+> | on(`open`, () => void) | Called just after the dialog is opened |
+> | on(`render`, () => void) | Called just after the dialog contents are rendered |
+> | on(`clear`, () => void) | Called just after the dialog contents are cleared |
+> | on(`destroy`, () => void) | Called just before the dialog is destroyed and all listeners are removed |
+> 
+> <details><summary><b>Example <i>(click to expand)</i></b></summary>
+> 
+> ```ts
+> const dialog = new unsafeWindow.BYTM.BytmDialog({
+>   id: "my-dialog",
+>   maxWidth: 500,
+>   maxHeight: 300,
+>   closeOnBgClick: true,
+>   closeOnEscPress: true,
+>   closeBtnEnabled: true,
+>   destroyOnClose: false,
+>   smallDialog: true,
+>   // add more elements to the header, body and footer here, with one of these methods:
+>   // - body.appendChild(document.createElement("..."));
+>   // - body.innerHTML = "..."
+>   // - ReactDOM.render(<MyComponent />, body);
+>   // - etc.
+>   renderHeader: () => {
+>     const header = document.createElement("div");
+>     header.textContent = "My Dialog";
+>     return header;
+>   },
+>   renderBody: () => {
+>     const body = document.createElement("div");
+>     body.textContent = "This is the body of my dialog";
+>     return body;
+>   },
+>   renderFooter: () => {
+>     const footer = document.createElement("div");
+>     footer.textContent = "This is the footer of my dialog";
+>     return footer;
+>   },
+> });
+> 
+> async function run() {
+>   dialog.on("close", () => {
+>     console.log("The dialog was closed");
+>   });
+> 
+>   await dialog.open();
+>   console.log("The dialog is now open");
+> }
+> 
+> run();
+> ```
+> </details>
+
+<br>
+
+> #### createHotkeyInput()
+> Usage:  
+> ```ts
+> unsafeWindow.BYTM.createHotkeyInput(inputProps: {
+>   initialValue?: HotkeyObj,
+>   onChange: (hotkey: HotkeyObj) => void,
+> }): HTMLElement
+> ```
+>   
+> Creates a hotkey input element that can be used to let the user set a hotkey.  
+> The HotkeyObj type has the properties `code: string`, `shift: boolean`, `ctrl: boolean` and `alt: boolean`  
+> The function `onChange` is called whenever the hotkey was changed.  
+>   
+> <details><summary><b>Example <i>(click to expand)</i></b></summary>
+> 
+> ```ts
+> const hotkeyInput = unsafeWindow.BYTM.createHotkeyInput({
+>   initialValue: { code: "KeyA", shift: true, ctrl: false, alt: false },
+>   onChange: (hotkey) => {
+>     console.log(`The hotkey was changed to ${hotkey.code} with shift: ${hotkey.shift}, ctrl: ${hotkey.ctrl} and alt: ${hotkey.alt}`);
+>   },
+> });
+> 
+> document.querySelector("#my-element").appendChild(hotkeyInput);
+> ```
+> </details>
+
+<br>
+
+> #### createToggleInput()
+> Usage:  
+> ```ts
+> unsafeWindow.BYTM.createToggleInput(toggleProps: {
+>   onChange: (value: boolean) => void,
+>   initialValue?: boolean,
+>   id?: string,
+>   labelPos?: "off" | "left" | "right",
+> })
+> ```
+>   
+> Creates a toggle input element that behaves like a checkbox but looks better.  
+> - `onChange` - Callback function that is called when the toggle is changed, gets passed the new value of the toggle as a boolean.
+> - `initialValue` - Initial value of the toggle - defaults to false (toggled off).
+> - `id` - Useful for getting a unique selector - if unspecified, a random ID is generated.
+> - `labelPos` - Toggle builtin label "off" or change position of the label to "right" or "left", relative to the toggle.
+> 
+> <details><summary><b>Example <i>(click to expand)</i></b></summary>
+> 
+> ```ts
+> const toggleInput = unsafeWindow.BYTM.createToggleInput({
+>   onChange: (value) => {
+>     console.log(`The toggle was changed to ${value}`);
+>   },
+>   initialValue: true,
+>   id: "my-toggle",
+>   labelPos: "left",
+> });
+> 
+> document.querySelector("#my-element").appendChild(toggleInput);
+> ```
+> </details>
+
 
 <br><br><br><br><br><br>

+ 10 - 8
src/components/BytmDialog.ts

@@ -206,8 +206,17 @@ export class BytmDialog extends NanoEmitter<{
     this.unsubscribeAll();
   }
 
+  //#MARKER static
+
+  /** Returns the ID of the top-most dialog (the dialog that has been opened last) */
+  public static getLastDialogId() {
+    return lastDialogId;
+  }
+
+  //#MARKER protected
+
   /** Called once to attach all generic event listeners */
-  public attachListeners(bgElem: HTMLElement) {
+  protected attachListeners(bgElem: HTMLElement) {
     if(this.listenersAttached)
       return;
     this.listenersAttached = true;
@@ -227,13 +236,6 @@ 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 */

+ 6 - 2
src/interface.ts

@@ -1,13 +1,13 @@
 import * as UserUtils from "@sv443-network/userutils";
 import { mode, branch, host, buildNumber, compressionFormat, scriptInfo } from "./constants";
-import { getResourceUrl, getSessionId, getVideoTime, log, setLocale, getLocale, hasKey, hasKeyFor, t, tp, type TrLocale } from "./utils";
+import { getResourceUrl, getSessionId, getVideoTime, log, setLocale, getLocale, hasKey, hasKeyFor, NanoEmitter, t, tp, type TrLocale } from "./utils";
 import { addSelectorListener } from "./observers";
 import { getFeatures, saveFeatures } from "./config";
 import { featInfo } from "./features";
 import { fetchLyricsUrlTop, getLyricsCacheEntry, sanitizeArtists, sanitizeSong, type LyricsCache } from "./features/lyrics";
 import type { SiteEventsMap } from "./siteEvents";
 import type { FeatureConfig, FeatureInfo, LyricsCacheEntry } from "./types";
-import type { BytmDialog } from "./components";
+import { BytmDialog, createHotkeyInput, createToggleInput } from "./components";
 
 const { getUnsafeWindow } = UserUtils;
 
@@ -74,6 +74,10 @@ export function initInterface() {
     ...scriptInfo,
     ...globalFuncs,
     UserUtils,
+    NanoEmitter,
+    BytmDialog,
+    createHotkeyInput,
+    createToggleInput,
   };
 
   for(const [key, value] of Object.entries(props))