123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- import type { Emitter } from "nanoevents";
- import type { Stringifiable } from "@sv443-network/userutils";
- import { getOS, resourceAsString, setInnerHtml, t } from "../utils/index.js";
- import { BytmDialog, type BytmDialogEvents } from "../components/index.js";
- import { addSelectorListener } from "../observers.js";
- import "./prompt.css";
- //#region types
- type PromptStringGen = Stringifiable | ((type: PromptType) => Stringifiable | Promise<Stringifiable>);
- export type PromptDialogRenderProps = ConfirmRenderProps | AlertRenderProps | PromptRenderProps;
- export type PromptType = PromptDialogRenderProps["type"];
- type ConfirmRenderProps = BaseRenderProps & {
- type: "confirm";
- };
- type AlertRenderProps = BaseRenderProps & {
- type: "alert";
- };
- type PromptRenderProps = BaseRenderProps & {
- type: "prompt";
- defaultValue?: string;
- };
- type BaseRenderProps = {
- message: PromptStringGen;
- confirmBtnText?: PromptStringGen;
- confirmBtnTooltip?: PromptStringGen;
- denyBtnText?: PromptStringGen;
- denyBtnTooltip?: PromptStringGen;
- };
- export type PromptDialogResolveVal = boolean | string | null;
- export type ShowPromptProps = Partial<PromptDialogRenderProps> & Required<Pick<PromptDialogRenderProps, "message">>;
- export type PromptDialogEmitter = Emitter<BytmDialogEvents & {
- resolve: (result: PromptDialogResolveVal) => void;
- }>;
- //#region PromptDialog
- let promptDialog: PromptDialog | null = null;
- class PromptDialog extends BytmDialog {
- constructor(props: PromptDialogRenderProps) {
- super({
- id: "prompt-dialog",
- width: 500,
- height: 400,
- destroyOnClose: true,
- closeBtnEnabled: true,
- closeOnBgClick: props.type === "alert",
- closeOnEscPress: true,
- small: true,
- renderHeader: () => this.renderHeader(props),
- renderBody: () => this.renderBody(props),
- renderFooter: () => this.renderFooter(props),
- });
- this.on("render", this.focusOnRender);
- }
- protected emitResolve(val: PromptDialogResolveVal) {
- this.events.emit("resolve", val);
- }
- protected async renderHeader({ type }: PromptDialogRenderProps) {
- const headerEl = document.createElement("div");
- headerEl.id = "bytm-prompt-dialog-header";
- const iconSvg = await resourceAsString(type === "alert" ? "icon-alert" : "icon-prompt");
- if(iconSvg)
- setInnerHtml(headerEl, iconSvg);
- return headerEl;
- }
- protected async renderBody({ type, message, ...rest }: PromptDialogRenderProps) {
- const contElem = document.createElement("div");
- contElem.classList.add(`bytm-prompt-type-${type}`);
- const upperContElem = document.createElement("div");
- upperContElem.id = "bytm-prompt-dialog-upper-cont";
- contElem.appendChild(upperContElem);
- const messageElem = document.createElement("p");
- messageElem.id = "bytm-prompt-dialog-message";
- messageElem.role = "alert";
- messageElem.ariaLive = "polite";
- messageElem.tabIndex = 0;
- messageElem.textContent = String(message);
- upperContElem.appendChild(messageElem);
- if(type === "prompt") {
- const inputElem = document.createElement("input");
- inputElem.id = "bytm-prompt-dialog-input";
- inputElem.type = "text";
- inputElem.autocomplete = "off";
- inputElem.spellcheck = false;
- inputElem.value = "defaultValue" in rest ? rest.defaultValue ?? "" : "";
- const inputEnterListener = (e: KeyboardEvent) => {
- if(e.key === "Enter") {
- inputElem.removeEventListener("keydown", inputEnterListener);
- this.emitResolve(inputElem?.value?.trim() ?? null);
- promptDialog?.close();
- }
- };
- inputElem.addEventListener("keydown", inputEnterListener);
- promptDialog?.once("close", () => inputElem.removeEventListener("keydown", inputEnterListener));
- upperContElem.appendChild(inputElem);
- }
- return contElem;
- }
- protected async renderFooter({ type, ...rest }: PromptDialogRenderProps) {
- const buttonsWrapper = document.createElement("div");
- buttonsWrapper.id = "bytm-prompt-dialog-button-wrapper";
- const buttonsCont = document.createElement("div");
- buttonsCont.id = "bytm-prompt-dialog-buttons-cont";
- let confirmBtn: HTMLButtonElement | undefined;
- if(type === "confirm" || type === "prompt") {
- confirmBtn = document.createElement("button");
- confirmBtn.id = "bytm-prompt-dialog-confirm";
- confirmBtn.classList.add("bytm-prompt-dialog-button");
- confirmBtn.textContent = await this.consumePromptStringGen(type, rest.confirmBtnText, t("prompt_confirm"));
- confirmBtn.ariaLabel = confirmBtn.title = await this.consumePromptStringGen(type, rest.confirmBtnTooltip, t("click_to_confirm_tooltip"));
- confirmBtn.tabIndex = 0;
- confirmBtn.addEventListener("click", () => {
- this.emitResolve(type === "confirm" ? true : (document.querySelector<HTMLInputElement>("#bytm-prompt-dialog-input"))?.value?.trim() ?? null);
- promptDialog?.close();
- }, { once: true });
- }
- const closeBtn = document.createElement("button");
- closeBtn.id = "bytm-prompt-dialog-close";
- closeBtn.classList.add("bytm-prompt-dialog-button");
- closeBtn.textContent = await this.consumePromptStringGen(type, rest.denyBtnText, t(type === "alert" ? "prompt_close" : "prompt_cancel"));
- closeBtn.ariaLabel = closeBtn.title = await this.consumePromptStringGen(type, rest.denyBtnTooltip, t(type === "alert" ? "click_to_close_tooltip" : "click_to_cancel_tooltip"));
- closeBtn.tabIndex = 0;
- closeBtn.addEventListener("click", () => {
- const resVals: Record<PromptType, boolean | null> = {
- alert: true,
- confirm: false,
- prompt: null,
- };
- this.emitResolve(resVals[type]);
- promptDialog?.close();
- }, { once: true });
- confirmBtn && getOS() !== "mac" && buttonsCont.appendChild(confirmBtn);
- buttonsCont.appendChild(closeBtn);
- confirmBtn && getOS() === "mac" && buttonsCont.appendChild(confirmBtn);
- buttonsWrapper.appendChild(buttonsCont);
- return buttonsWrapper;
- }
- /** Converts a {@linkcode stringGen} (stringifiable value or sync or async function that returns a stringifiable value) to a string - uses {@linkcode fallback} as a fallback */
- protected async consumePromptStringGen(curPromptType: PromptType, stringGen?: PromptStringGen, fallback?: Stringifiable): Promise<string> {
- if(typeof stringGen === "function")
- return await stringGen(curPromptType);
- return String(stringGen ?? fallback);
- }
- /** Called on render to focus on the confirm or cancel button or text input, depending on prompt type */
- protected focusOnRender() {
- const inputElem = document.querySelector<HTMLInputElement>("#bytm-prompt-dialog-input");
- if(inputElem)
- return inputElem.focus();
- let captureEnterKey = true;
- document.addEventListener("keydown", (e) => {
- if(e.key === "Enter" && captureEnterKey) {
- const confBtn = document.querySelector<HTMLButtonElement>("#bytm-prompt-dialog-confirm");
- const closeBtn = document.querySelector<HTMLButtonElement>("#bytm-prompt-dialog-close");
- if(confBtn || closeBtn) {
- confBtn?.click() ?? closeBtn?.click();
- captureEnterKey = false;
- }
- }
- }, { capture: true, once: true });
- }
- }
- //#region showPrompt fn
- /** Shows a `confirm()`-like prompt dialog with the specified message and resolves true if the user confirms it or false if they deny or cancel it */
- export function showPrompt(props: ConfirmRenderProps): Promise<boolean>;
- /** Shows an `alert()`-like prompt dialog with the specified message and always resolves true once the user dismisses it */
- export function showPrompt(props: AlertRenderProps): Promise<true>;
- /** Shows a `prompt()`-like dialog with the specified message and default value and resolves the entered value if the user confirms it or null if they cancel it */
- export function showPrompt(props: PromptRenderProps): Promise<string | null>;
- /** Custom dialog to emulate and enhance the behavior of the native `confirm()`, `alert()`, and `prompt()` functions */
- export function showPrompt({ type, ...rest }: PromptDialogRenderProps): Promise<PromptDialogResolveVal> {
- return new Promise<PromptDialogResolveVal>((resolve) => {
- if(BytmDialog.getOpenDialogs().includes("prompt-dialog"))
- promptDialog?.close();
- promptDialog = new PromptDialog({ type, ...rest });
- promptDialog.once("render" as "_", () => {
- addSelectorListener<HTMLButtonElement>("bytmDialogContainer", `#bytm-prompt-dialog-${type === "alert" ? "close" : "confirm"}`, {
- listener: (btn) => btn.focus(),
- });
- });
- // make config menu inert while prompt dialog is open
- promptDialog.once("open", () => document.querySelector("#bytm-cfg-menu")?.setAttribute("inert", "true"));
- promptDialog.once("close", () => document.querySelector("#bytm-cfg-menu")?.removeAttribute("inert"));
- let resolveVal: PromptDialogResolveVal | undefined;
- const tryResolve = () => resolve(typeof resolveVal !== "undefined" ? resolveVal : false);
- let closeUnsub: (() => void) | undefined; // eslint-disable-line prefer-const
- const resolveUnsub = promptDialog.on("resolve" as "_", (val: PromptDialogResolveVal) => {
- resolveUnsub();
- if(resolveVal !== undefined)
- return;
- resolveVal = val;
- tryResolve();
- closeUnsub?.();
- });
- closeUnsub = promptDialog.on("close", () => {
- closeUnsub!();
- if(resolveVal !== undefined)
- return;
- resolveVal = type === "alert";
- if(type === "prompt")
- resolveVal = null;
- tryResolve();
- resolveUnsub();
- });
- promptDialog.open();
- });
- }
|