blessing-skin-server/resources/assets/src/components/Modal.tsx
2024-03-05 10:28:07 +08:00

164 lines
4.0 KiB
TypeScript

import {useState, useEffect, useRef} from 'react';
import $ from 'jquery';
import 'bootstrap';
import {t} from '../scripts/i18n';
import ModalHeader, {type Props as HeaderProperties} from './ModalHeader';
import ModalBody, {type Props as BodyProperties} from './ModalBody';
import ModalFooter, {type Props as FooterProperties} from './ModalFooter';
type BasicOptions = {
readonly mode?: 'alert' | 'confirm' | 'prompt';
readonly show?: boolean;
readonly input?: string;
validator?(value: any): string | boolean | undefined;
readonly type?: string;
readonly showHeader?: boolean;
readonly center?: boolean;
children?: React.ReactNode;
};
export type ModalOptions = BasicOptions & HeaderProperties & BodyProperties & FooterProperties;
type Properties = {
readonly id?: string;
readonly children?: React.ReactNode;
readonly footer?: React.ReactNode;
onConfirm?(payload: {value: string}): void;
onDismiss?(): void;
onClose?(): void;
};
export type ModalResult = {
value: string;
};
const Modal: React.FC<ModalOptions & Properties> = properties => {
const {
mode = 'confirm',
title = t('general.tip'),
text = '',
input = '',
placeholder = '',
inputType = 'text',
inputMode,
type = 'default',
showHeader = true,
center = false,
okButtonText = t('general.confirm'),
okButtonType = 'primary',
cancelButtonText = t('general.cancel'),
cancelButtonType = 'secondary',
flexFooter = false,
} = properties;
const [value, setValue] = useState(input);
const [valid, setValid] = useState(true);
const [validatorMessage, setValidatorMessage] = useState('');
const reference = useRef<HTMLDivElement>(null);
const {show, onClose} = properties;
useEffect(() => {
if (!show) {
return;
}
const onHidden = () => {
onClose ? onClose() : void 0;
};
const element = $(reference.current!);
element.on('hidden.bs.modal', onHidden);
return () => {
element.off('hidden.bs.modal', onHidden);
};
}, [show, onClose]);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
const confirm = () => {
const {validator} = properties;
if (typeof validator === 'function') {
const result = validator(value);
if (typeof result === 'string') {
setValidatorMessage(result);
setValid(false);
return;
}
}
properties.onConfirm?.({value});
$(reference.current!).modal('hide');
// The "hidden.bs.modal" event can't be trigged automatically when testing.
if (process.env.NODE_ENV === 'test') {
$(reference.current!).trigger('hidden.bs.modal');
}
};
const dismiss = () => {
properties.onDismiss?.();
$(reference.current!).modal('hide');
if (process.env.NODE_ENV === 'test') {
$(reference.current!).trigger('hidden.bs.modal');
}
};
useEffect(() => {
if (show) {
setTimeout(() => $(reference.current!).modal('show'), 50);
}
}, [show]);
if (!show) {
return null;
}
return (
<div ref={reference} id={properties.id} className='modal fade' role='dialog'>
<div
className={`modal-dialog ${center ? 'modal-dialog-centered' : ''}`}
role='document'
>
<div className={`modal-content bg-${type}`}>
<ModalHeader show={showHeader} title={title} onDismiss={dismiss}/>
<ModalBody
text={text}
dangerousHTML={properties.dangerousHTML}
showInput={mode === 'prompt'}
value={value}
choices={properties.choices}
inputType={inputType}
inputMode={inputMode}
placeholder={placeholder}
invalid={!valid}
validatorMessage={validatorMessage}
onChange={handleInputChange}
>
{properties.children}
</ModalBody>
<ModalFooter
showCancelButton={mode !== 'alert'}
flexFooter={flexFooter}
okButtonType={okButtonType}
okButtonText={okButtonText}
cancelButtonType={cancelButtonType}
cancelButtonText={cancelButtonText}
onConfirm={confirm}
onDismiss={dismiss}
>
{properties.footer}
</ModalFooter>
</div>
</div>
</div>
);
};
export default Modal;