prompt.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  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 = {
  7. type: "confirm" | "alert";
  8. message: Stringifiable;
  9. };
  10. export type ShowPromptProps = Partial<PromptDialogRenderProps> & Required<Pick<PromptDialogRenderProps, "message">>;
  11. export type PromptDialogEmitter = Emitter<BytmDialogEvents & {
  12. resolve: (result: boolean) => void;
  13. }>;
  14. let promptDialog: PromptDialog | null = null;
  15. // TODO: implement prompt() equivalent for text input
  16. class PromptDialog extends BytmDialog {
  17. constructor(props: PromptDialogRenderProps) {
  18. super({
  19. id: "prompt-dialog",
  20. width: 400,
  21. height: 400,
  22. destroyOnClose: true,
  23. closeBtnEnabled: true,
  24. closeOnBgClick: props.type === "alert",
  25. closeOnEscPress: true,
  26. small: true,
  27. renderHeader: () => this.renderHeader(props),
  28. renderBody: () => this.renderBody(props),
  29. });
  30. }
  31. async renderHeader({ type }: PromptDialogRenderProps) {
  32. const headerEl = document.createElement("div");
  33. headerEl.id = "bytm-prompt-dialog-header";
  34. const iconSvg = await resourceAsString(type === "alert" ? "icon-alert" : "icon-confirm");
  35. if(iconSvg)
  36. setInnerHtml(headerEl, iconSvg);
  37. return headerEl;
  38. }
  39. async renderBody({ type, message }: PromptDialogRenderProps) {
  40. const resolve = (val: boolean) => (this.events as PromptDialogEmitter).emit("resolve", val);
  41. const contElem = document.createElement("div");
  42. const messageElem = document.createElement("h3");
  43. messageElem.role = "subheading";
  44. messageElem.tabIndex = 0;
  45. messageElem.textContent = String(message);
  46. messageElem.id = "bytm-prompt-dialog-message";
  47. contElem.appendChild(messageElem);
  48. const buttonsWrapper = document.createElement("div");
  49. buttonsWrapper.id = "bytm-prompt-dialog-button-wrapper";
  50. const buttonsCont = document.createElement("div");
  51. buttonsCont.id = "bytm-prompt-dialog-buttons-cont";
  52. let confirmBtn: HTMLButtonElement | undefined;
  53. if(type === "confirm") {
  54. confirmBtn = document.createElement("button");
  55. confirmBtn.textContent = confirmBtn.ariaLabel = confirmBtn.title = t("prompt_confirm");
  56. confirmBtn.id = "bytm-prompt-dialog-confirm";
  57. confirmBtn.tabIndex = 0;
  58. confirmBtn.addEventListener("click", () => {
  59. resolve(true);
  60. promptDialog?.close();
  61. }, { once: true });
  62. }
  63. const closeBtn = document.createElement("button");
  64. closeBtn.textContent = closeBtn.ariaLabel = closeBtn.title = t(type === "alert" ? "prompt_close" : "prompt_cancel");
  65. closeBtn.id = "bytm-prompt-dialog-close";
  66. closeBtn.tabIndex = 0;
  67. closeBtn.addEventListener("click", () => {
  68. resolve(type === "alert");
  69. promptDialog?.close();
  70. }, { once: true });
  71. confirmBtn && getOS() !== "mac" && buttonsCont.appendChild(confirmBtn);
  72. buttonsCont.appendChild(closeBtn);
  73. confirmBtn && getOS() === "mac" && buttonsCont.appendChild(confirmBtn);
  74. buttonsWrapper.appendChild(buttonsCont);
  75. contElem.appendChild(buttonsWrapper);
  76. return contElem;
  77. }
  78. }
  79. /** Shows a 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" */
  80. export function showPrompt({
  81. type = "alert",
  82. message,
  83. }: ShowPromptProps) {
  84. return new Promise<boolean>((resolve) => {
  85. if(BytmDialog.getOpenDialogs().includes("prompt-dialog"))
  86. promptDialog?.close();
  87. promptDialog = new PromptDialog({
  88. type,
  89. message,
  90. });
  91. // make config menu inert while prompt dialog is open
  92. promptDialog.once("open", () => document.querySelector("#bytm-cfg-menu")?.setAttribute("inert", "true"));
  93. promptDialog.once("close", () => document.querySelector("#bytm-cfg-menu")?.removeAttribute("inert"));
  94. let resolveVal: boolean | undefined;
  95. const tryResolve = () => resolve(typeof resolveVal === "boolean" ? resolveVal : false);
  96. const resolveUnsub = promptDialog.on("resolve" as "_", (val: boolean) => {
  97. resolveUnsub();
  98. if(resolveVal)
  99. return;
  100. resolveVal = val;
  101. tryResolve();
  102. });
  103. const closeUnsub = promptDialog.on("close", () => {
  104. closeUnsub();
  105. if(resolveVal)
  106. return;
  107. resolveVal = type === "alert";
  108. tryResolve();
  109. });
  110. promptDialog.open();
  111. });
  112. }
  113. //@ts-ignore
  114. unsafeWindow.showPrompt = showPrompt;