prompt.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import type { Emitter } from "nanoevents";
  2. import type { Stringifiable } from "@sv443-network/userutils";
  3. import { getOS, resourceAsString, setInnerHtml, t } from "../utils/index.js";
  4. import { BytmDialog, type BytmDialogEvents } from "../components/index.js";
  5. import "./prompt.css";
  6. export type PromptDialogRenderProps = ConfirmRenderProps | AlertRenderProps | PromptRenderProps;
  7. type ConfirmRenderProps = BaseRenderProps & {
  8. type: "confirm";
  9. };
  10. type AlertRenderProps = BaseRenderProps & {
  11. type: "alert";
  12. };
  13. type PromptRenderProps = BaseRenderProps & {
  14. type: "prompt";
  15. defaultValue?: string;
  16. };
  17. type BaseRenderProps = {
  18. message: Stringifiable;
  19. };
  20. export type ShowPromptProps = Partial<PromptDialogRenderProps> & Required<Pick<PromptDialogRenderProps, "message">>;
  21. export type PromptDialogEmitter = Emitter<BytmDialogEvents & {
  22. resolve: (result: boolean | string | null) => void;
  23. }>;
  24. let promptDialog: PromptDialog | null = null;
  25. class PromptDialog extends BytmDialog {
  26. constructor(props: PromptDialogRenderProps) {
  27. super({
  28. id: "prompt-dialog",
  29. width: 500,
  30. height: 400,
  31. destroyOnClose: true,
  32. closeBtnEnabled: true,
  33. closeOnBgClick: props.type === "alert",
  34. closeOnEscPress: true,
  35. small: true,
  36. renderHeader: () => this.renderHeader(props),
  37. renderBody: () => this.renderBody(props),
  38. });
  39. }
  40. async renderHeader({ type }: PromptDialogRenderProps) {
  41. const headerEl = document.createElement("div");
  42. headerEl.id = "bytm-prompt-dialog-header";
  43. const iconSvg = await resourceAsString(type === "alert" ? "icon-alert" : "icon-confirm");
  44. if(iconSvg)
  45. setInnerHtml(headerEl, iconSvg);
  46. return headerEl;
  47. }
  48. async renderBody({ type, message, ...rest }: PromptDialogRenderProps) {
  49. const resolve = (val: boolean | string | null) => (this.events as PromptDialogEmitter).emit("resolve", val);
  50. const contElem = document.createElement("div");
  51. const upperContElem = document.createElement("div");
  52. upperContElem.id = "bytm-prompt-dialog-upper-cont";
  53. contElem.appendChild(upperContElem);
  54. const messageElem = document.createElement("p");
  55. messageElem.id = "bytm-prompt-dialog-message";
  56. messageElem.role = "alert";
  57. messageElem.tabIndex = 0;
  58. messageElem.textContent = String(message);
  59. upperContElem.appendChild(messageElem);
  60. if(type === "prompt") {
  61. const inputElem = document.createElement("input");
  62. inputElem.id = "bytm-prompt-dialog-input";
  63. inputElem.type = "text";
  64. inputElem.autocomplete = "off";
  65. inputElem.spellcheck = false;
  66. inputElem.value = "defaultValue" in rest ? rest.defaultValue ?? "" : "";
  67. inputElem.autofocus = true;
  68. upperContElem.appendChild(inputElem);
  69. }
  70. const buttonsWrapper = document.createElement("div");
  71. buttonsWrapper.id = "bytm-prompt-dialog-button-wrapper";
  72. const buttonsCont = document.createElement("div");
  73. buttonsCont.id = "bytm-prompt-dialog-buttons-cont";
  74. let confirmBtn: HTMLButtonElement | undefined;
  75. if(type === "confirm" || type === "prompt") {
  76. confirmBtn = document.createElement("button");
  77. confirmBtn.id = "bytm-prompt-dialog-confirm";
  78. confirmBtn.classList.add("bytm-prompt-dialog-button");
  79. confirmBtn.textContent = t("prompt_confirm");
  80. confirmBtn.ariaLabel = confirmBtn.title = t("click_to_confirm_tooltip");
  81. confirmBtn.tabIndex = 0;
  82. confirmBtn.autofocus = type === "confirm";
  83. confirmBtn.addEventListener("click", () => {
  84. resolve(type === "confirm" ? true : (document.querySelector<HTMLInputElement>("#bytm-prompt-dialog-input"))?.value?.trim() ?? null);
  85. promptDialog?.close();
  86. }, { once: true });
  87. }
  88. const closeBtn = document.createElement("button");
  89. closeBtn.id = "bytm-prompt-dialog-close";
  90. closeBtn.classList.add("bytm-prompt-dialog-button");
  91. closeBtn.textContent = t(type === "alert" ? "prompt_close" : "prompt_cancel");
  92. closeBtn.ariaLabel = closeBtn.title = t(type === "alert" ? "click_to_close_tooltip" : "click_to_cancel_tooltip");
  93. closeBtn.tabIndex = 0;
  94. if(type === "alert")
  95. closeBtn.autofocus = true;
  96. closeBtn.addEventListener("click", () => {
  97. const resVals: Record<PromptDialogRenderProps["type"], boolean | null> = {
  98. alert: true,
  99. confirm: false,
  100. prompt: null,
  101. };
  102. resolve(resVals[type]);
  103. promptDialog?.close();
  104. }, { once: true });
  105. confirmBtn && getOS() !== "mac" && buttonsCont.appendChild(confirmBtn);
  106. buttonsCont.appendChild(closeBtn);
  107. confirmBtn && getOS() === "mac" && buttonsCont.appendChild(confirmBtn);
  108. buttonsWrapper.appendChild(buttonsCont);
  109. contElem.appendChild(buttonsWrapper);
  110. return contElem;
  111. }
  112. }
  113. /** Shows a confirm() or alert() prompt dialog of the specified type and resolves true if the user confirms it or false if they cancel it - always resolves true with type "alert" */
  114. export function showPrompt(props: ConfirmRenderProps | AlertRenderProps): Promise<boolean>;
  115. /** Shows a prompt() dialog with the specified message and default value and resolves the entered value if the user confirms it or null if they cancel it */
  116. export function showPrompt(props: PromptRenderProps): Promise<string | null>;
  117. export function showPrompt({ type, ...rest }: PromptDialogRenderProps): Promise<boolean | string | null> {
  118. return new Promise<boolean | string | null>((resolve) => {
  119. if(BytmDialog.getOpenDialogs().includes("prompt-dialog"))
  120. promptDialog?.close();
  121. promptDialog = new PromptDialog({ type, ...rest });
  122. // make config menu inert while prompt dialog is open
  123. promptDialog.once("open", () => document.querySelector("#bytm-cfg-menu")?.setAttribute("inert", "true"));
  124. promptDialog.once("close", () => document.querySelector("#bytm-cfg-menu")?.removeAttribute("inert"));
  125. let resolveVal: boolean | string | null | undefined;
  126. const tryResolve = () => resolve(typeof resolveVal !== "undefined" ? resolveVal : false);
  127. const resolveUnsub = promptDialog.on("resolve" as "_", (val: boolean | string | null) => {
  128. resolveUnsub();
  129. if(resolveVal !== undefined)
  130. return;
  131. resolveVal = val;
  132. tryResolve();
  133. });
  134. const closeUnsub = promptDialog.on("close", () => {
  135. closeUnsub();
  136. if(resolveVal !== undefined)
  137. return;
  138. resolveVal = type === "alert";
  139. if(type === "prompt")
  140. resolveVal = null;
  141. tryResolve();
  142. });
  143. promptDialog.open();
  144. });
  145. }