feat(react/dialogs): port prompt

This commit is contained in:
Elian Doran 2025-08-05 19:06:47 +03:00
parent b3c81ce5f2
commit bde4545afc
No known key found for this signature in database
12 changed files with 117 additions and 132 deletions

View File

@ -244,7 +244,7 @@
"prompt": {
"title": "提示",
"close": "关闭",
"ok": "确定 <kbd>回车</kbd>",
"ok": "确定",
"defaultTitle": "提示"
},
"protected_session_password": {

View File

@ -244,7 +244,7 @@
"prompt": {
"title": "Prompt",
"close": "Schließen",
"ok": "OK <kbd>Eingabe</kbd>",
"ok": "OK",
"defaultTitle": "Prompt"
},
"protected_session_password": {

View File

@ -250,7 +250,7 @@
"prompt": {
"title": "Prompt",
"close": "Close",
"ok": "OK <kbd>enter</kbd>",
"ok": "OK",
"defaultTitle": "Prompt"
},
"protected_session_password": {

View File

@ -247,7 +247,7 @@
"prompt": {
"title": "Aviso",
"close": "Cerrar",
"ok": "Aceptar <kbd>enter</kbd>",
"ok": "Aceptar",
"defaultTitle": "Aviso"
},
"protected_session_password": {

View File

@ -244,7 +244,7 @@
"prompt": {
"title": "Prompt",
"close": "Fermer",
"ok": "OK <kbd>entrer</kbd>",
"ok": "OK",
"defaultTitle": "Prompt"
},
"protected_session_password": {

View File

@ -970,7 +970,7 @@
},
"prompt": {
"defaultTitle": "Aviz",
"ok": "OK <kbd>enter</kbd>",
"ok": "OK",
"title": "Aviz",
"close": "Închide"
},

View File

@ -222,7 +222,7 @@
},
"prompt": {
"title": "提示",
"ok": "確定 <kbd>Enter</kbd>",
"ok": "確定",
"defaultTitle": "提示"
},
"protected_session_password": {

View File

@ -1,115 +0,0 @@
import { openDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js";
import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap";
const TPL = /*html*/`
<div class="prompt-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form class="prompt-dialog-form">
<div class="modal-header">
<h5 class="prompt-title modal-title">${t("prompt.title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("prompt.close")}"></button>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button class="prompt-dialog-ok-button btn btn-primary btn-sm">${t("prompt.ok")}</button>
</div>
</form>
</div>
</div>
</div>`;
interface ShownCallbackData {
$dialog: JQuery<HTMLElement>;
$question: JQuery<HTMLElement> | null;
$answer: JQuery<HTMLElement> | null;
$form: JQuery<HTMLElement>;
}
export interface PromptDialogOptions {
title?: string;
message?: string;
defaultValue?: string;
shown?: PromptShownDialogCallback;
callback?: (value: string | null) => void;
}
export type PromptShownDialogCallback = ((callback: ShownCallbackData) => void) | null;
export default class PromptDialog extends BasicWidget {
private resolve?: ((value: string | null) => void) | undefined | null;
private shownCb?: PromptShownDialogCallback | null;
private modal!: Modal;
private $dialogBody!: JQuery<HTMLElement>;
private $question!: JQuery<HTMLElement> | null;
private $answer!: JQuery<HTMLElement> | null;
private $form!: JQuery<HTMLElement>;
constructor() {
super();
this.resolve = null;
this.shownCb = null;
}
doRender() {
this.$widget = $(TPL);
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
this.$dialogBody = this.$widget.find(".modal-body");
this.$form = this.$widget.find(".prompt-dialog-form");
this.$question = null;
this.$answer = null;
this.$widget.on("shown.bs.modal", () => {
if (this.shownCb) {
this.shownCb({
$dialog: this.$widget,
$question: this.$question,
$answer: this.$answer,
$form: this.$form
});
}
this.$answer?.trigger("focus").select();
});
this.$widget.on("hidden.bs.modal", () => {
if (this.resolve) {
this.resolve(null);
}
});
this.$form.on("submit", (e) => {
e.preventDefault();
if (this.resolve) {
this.resolve(this.$answer?.val() as string);
}
this.modal.hide();
});
}
showPromptDialogEvent({ title, message, defaultValue, shown, callback }: PromptDialogOptions) {
this.shownCb = shown;
this.resolve = callback;
this.$widget.find(".prompt-title").text(title || t("prompt.defaultTitle"));
this.$question = $("<label>")
.prop("for", "prompt-dialog-answer")
.text(message || "");
this.$answer = $("<input>")
.prop("type", "text")
.prop("id", "prompt-dialog-answer")
.addClass("form-control")
.val(defaultValue || "");
this.$dialogBody.empty().append($("<div>").addClass("form-group").append(this.$question).append(this.$answer));
openDialog(this.$widget, false);
}
}

View File

@ -0,0 +1,87 @@
import { useRef, useState } from "preact/hooks";
import { closeActiveDialog, openDialog } from "../../services/dialog";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import Modal from "../react/Modal";
import { Modal as BootstrapModal } from "bootstrap";
import ReactBasicWidget from "../react/ReactBasicWidget";
import FormTextBox from "../react/FormTextBox";
import FormGroup from "../react/FormGroup";
// JQuery here is maintained for compatibility with existing code.
interface ShownCallbackData {
$dialog: JQuery<HTMLDivElement>;
$question: JQuery<HTMLLabelElement> | null;
$answer: JQuery<HTMLElement> | null;
$form: JQuery<HTMLFormElement>;
}
export type PromptShownDialogCallback = ((callback: ShownCallbackData) => void) | null;
export interface PromptDialogOptions {
title?: string;
message?: string;
defaultValue?: string;
shown?: PromptShownDialogCallback;
callback?: (value: string | null) => void;
}
interface PromptDialogProps extends PromptDialogOptions { }
function PromptDialogComponent({ title, message, shown: shownCallback, callback }: PromptDialogProps) {
const modalRef = useRef<HTMLDivElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const labelRef = useRef<HTMLLabelElement>(null);
const answerRef = useRef<HTMLInputElement>(null);
const [ value, setValue ] = useState("");
return (
<Modal
className="prompt-dialog"
title={title ?? t("prompt.title")}
size="lg"
zIndex={2000}
modalRef={modalRef} formRef={formRef}
onShown={() => {
shownCallback?.({
$dialog: $(modalRef.current),
$question: $(labelRef.current),
$answer: $(answerRef.current),
$form: $(formRef.current) as JQuery<HTMLFormElement>
});
answerRef.current?.focus();
}}
onSubmit={() => {
const modal = BootstrapModal.getOrCreateInstance(modalRef.current!);
modal.hide();
callback?.(value);
}}
onHidden={() => callback?.(null)}
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" primary />}
>
<FormGroup label={message} labelRef={labelRef}>
<FormTextBox
name="prompt-dialog-answer"
inputRef={answerRef}
currentValue={value} onChange={setValue} />
</FormGroup>
</Modal>
);
}
export default class PromptDialog extends ReactBasicWidget {
private props: PromptDialogProps;
get component() {
return <PromptDialogComponent {...this.props} />;
}
showPromptDialogEvent(props: PromptDialogOptions) {
this.props = props;
this.doRender();
openDialog(this.$widget, false);
}
}

View File

@ -1,6 +1,7 @@
import { ComponentChildren } from "preact";
import { ComponentChildren, RefObject } from "preact";
interface FormGroupProps {
labelRef?: RefObject<HTMLLabelElement>;
label: string;
title?: string;
className?: string;
@ -8,10 +9,10 @@ interface FormGroupProps {
description?: string;
}
export default function FormGroup({ label, title, className, children, description }: FormGroupProps) {
export default function FormGroup({ label, title, className, children, description, labelRef }: FormGroupProps) {
return (
<div className={`form-group ${className}`} title={title}>
<label style={{ width: "100%" }}>
<label style={{ width: "100%" }} ref={labelRef}>
{label}
{children}
</label>

View File

@ -1,4 +1,4 @@
import { HTMLInputTypeAttribute } from "preact/compat";
import { HTMLInputTypeAttribute, RefObject } from "preact/compat";
interface FormTextBoxProps {
id?: string;
@ -8,13 +8,15 @@ interface FormTextBoxProps {
className?: string;
autoComplete?: string;
onChange?(newValue: string): void;
inputRef?: RefObject<HTMLInputElement>;
}
export default function FormTextBox({ id, type, name, className, currentValue, onChange, autoComplete }: FormTextBoxProps) {
export default function FormTextBox({ id, type, name, className, currentValue, onChange, autoComplete, inputRef }: FormTextBoxProps) {
return (
<input
ref={inputRef}
type={type ?? "text"}
className={`form-control ${className}`}
className={`form-control ${className ?? ""}`}
id={id}
name={name}
value={currentValue}

View File

@ -1,7 +1,7 @@
import { useEffect, useRef } from "preact/hooks";
import { t } from "../../services/i18n";
import { ComponentChildren } from "preact";
import type { CSSProperties } from "preact/compat";
import type { CSSProperties, RefObject } from "preact/compat";
interface ModalProps {
className: string;
@ -29,10 +29,20 @@ interface ModalProps {
/** Called when the modal is hidden, either via close button, backdrop click or submit. */
onHidden?: () => void;
helpPageId?: string;
/**
* Gives access to the underlying modal element. This is useful for manipulating the modal directly
* or for attaching event listeners.
*/
modalRef?: RefObject<HTMLDivElement>;
/**
* Gives access to the underlying form element of the modal. This is only set if `onSubmit` is provided.
*/
formRef?: RefObject<HTMLFormElement>;
}
export default function Modal({ children, className, size, title, footer, footerAlignment, onShown, onSubmit, helpPageId, maxWidth, zIndex, scrollable, onHidden: onHidden }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
export default function Modal({ children, className, size, title, footer, footerAlignment, onShown, onSubmit, helpPageId, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: _modalRef, formRef: _formRef }: ModalProps) {
const modalRef = _modalRef ?? useRef<HTMLDivElement>(null);
const formRef = _formRef ?? useRef<HTMLFormElement>(null);
if (onShown || onHidden) {
useEffect(() => {
@ -84,7 +94,7 @@ export default function Modal({ children, className, size, title, footer, footer
</div>
{onSubmit ? (
<form onSubmit={(e) => {
<form ref={formRef} onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}>