feat: Удаление устаревших файлов документации и оптимизация структуры проекта. Упрощение кода и улучшение читаемости за счет удаления ненужных компонентов и отчетов, что способствует более эффективному управлению проектом.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m34s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m34s
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { IconCheck, IconX, IconAlertCircle } from '@tabler/icons-react';
|
||||
|
||||
/**
|
||||
* FormField - универсальное поле формы с валидацией
|
||||
* Поддерживает иконки, подсказки, ошибки и success состояния
|
||||
* Объединяет функциональность FormField и ValidatedInput
|
||||
* Поддерживает: иконки, подсказки, ошибки, success состояния, debounce валидацию
|
||||
*/
|
||||
function FormField({
|
||||
label,
|
||||
@@ -9,32 +13,76 @@ function FormField({
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
onValidate, // функция валидации (опционально): (value) => { valid: boolean, message: string }
|
||||
error,
|
||||
success,
|
||||
helpText,
|
||||
required,
|
||||
disabled,
|
||||
placeholder,
|
||||
autoFocus,
|
||||
icon: Icon,
|
||||
className = '',
|
||||
inputClassName = '',
|
||||
rows, // для textarea
|
||||
options, // для select
|
||||
debounceMs = 300, // debounce для валидации
|
||||
showValidationIcon = true,
|
||||
...inputProps
|
||||
}) {
|
||||
const inputId = `field-${name}`;
|
||||
const hasError = !!error;
|
||||
const hasSuccess = !!success && !error;
|
||||
const [localValue, setLocalValue] = useState(value || '');
|
||||
const [validation, setValidation] = useState({ valid: null, message: '' });
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const timerRef = useRef(null);
|
||||
|
||||
const hasExternalError = !!error;
|
||||
const hasValidationError = isDirty && validation.valid === false;
|
||||
const hasError = hasExternalError || hasValidationError;
|
||||
|
||||
const hasExternalSuccess = !!success && !hasExternalError;
|
||||
const hasValidationSuccess = isDirty && validation.valid === true && !hasExternalError;
|
||||
const hasSuccess = hasExternalSuccess || hasValidationSuccess;
|
||||
|
||||
const isTextarea = type === 'textarea';
|
||||
const isSelect = type === 'select';
|
||||
|
||||
// Синхронизация с внешним value
|
||||
useEffect(() => {
|
||||
setLocalValue(value || '');
|
||||
}, [value]);
|
||||
|
||||
// Валидация с debounce
|
||||
useEffect(() => {
|
||||
if (!isDirty || !onValidate) return;
|
||||
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
const result = onValidate(localValue);
|
||||
setValidation(result);
|
||||
}, debounceMs);
|
||||
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
};
|
||||
}, [localValue, isDirty, onValidate, debounceMs]);
|
||||
|
||||
const inputClasses = `form-control ${hasError ? 'is-invalid' : ''} ${hasSuccess ? 'is-valid' : ''} ${inputClassName}`;
|
||||
|
||||
const handleChange = (e) => {
|
||||
onChange?.(e.target.value, e);
|
||||
const newValue = e.target.value;
|
||||
setLocalValue(newValue);
|
||||
setIsDirty(true);
|
||||
onChange?.(newValue, e);
|
||||
};
|
||||
|
||||
const handleBlur = (e) => {
|
||||
setIsDirty(true);
|
||||
onBlur?.(e);
|
||||
};
|
||||
|
||||
@@ -45,13 +93,14 @@ function FormField({
|
||||
id={inputId}
|
||||
name={name}
|
||||
className={inputClasses}
|
||||
value={value}
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
rows={rows || 3}
|
||||
autoFocus={autoFocus}
|
||||
aria-invalid={hasError}
|
||||
aria-describedby={error ? `${inputId}-error` : helpText ? `${inputId}-help` : undefined}
|
||||
{...inputProps}
|
||||
@@ -65,11 +114,12 @@ function FormField({
|
||||
id={inputId}
|
||||
name={name}
|
||||
className={inputClasses.replace('form-control', 'form-select')}
|
||||
value={value}
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
autoFocus={autoFocus}
|
||||
aria-invalid={hasError}
|
||||
aria-describedby={error ? `${inputId}-error` : helpText ? `${inputId}-help` : undefined}
|
||||
{...inputProps}
|
||||
@@ -89,12 +139,13 @@ function FormField({
|
||||
name={name}
|
||||
type={type}
|
||||
className={inputClasses}
|
||||
value={value}
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
autoFocus={autoFocus}
|
||||
aria-invalid={hasError}
|
||||
aria-describedby={error ? `${inputId}-error` : helpText ? `${inputId}-help` : undefined}
|
||||
{...inputProps}
|
||||
@@ -102,6 +153,53 @@ function FormField({
|
||||
);
|
||||
};
|
||||
|
||||
// Рендер с иконкой (слева или справа для валидации)
|
||||
const renderInputWithIcon = () => {
|
||||
const hasValidationIcon = showValidationIcon && isDirty && validation.valid !== null;
|
||||
|
||||
if (Icon) {
|
||||
// Иконка слева (переданная через prop)
|
||||
return (
|
||||
<div className="input-icon">
|
||||
<span className="input-icon-addon">
|
||||
<Icon size={18} />
|
||||
</span>
|
||||
{renderInput()}
|
||||
{hasValidationIcon && (
|
||||
<span className="input-icon-addon" style={{ right: 0, left: 'auto' }}>
|
||||
{validation.valid === true ? (
|
||||
<IconCheck size={20} className="text-success" />
|
||||
) : (
|
||||
<IconX size={20} className="text-danger" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasValidationIcon) {
|
||||
// Только иконка валидации справа
|
||||
return (
|
||||
<div className="input-icon">
|
||||
{renderInput()}
|
||||
<span className="input-icon-addon">
|
||||
{validation.valid === true ? (
|
||||
<IconCheck size={20} className="text-success" />
|
||||
) : (
|
||||
<IconX size={20} className="text-danger" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return renderInput();
|
||||
};
|
||||
|
||||
const displayError = error || (hasValidationError ? validation.message : null);
|
||||
const displaySuccess = success || (hasValidationSuccess ? validation.message : null);
|
||||
|
||||
return (
|
||||
<div className={`mb-3 ${className}`}>
|
||||
{label && (
|
||||
@@ -111,31 +209,23 @@ function FormField({
|
||||
</label>
|
||||
)}
|
||||
|
||||
{Icon ? (
|
||||
<div className="input-icon">
|
||||
<span className="input-icon-addon">
|
||||
<Icon size={18} />
|
||||
</span>
|
||||
{renderInput()}
|
||||
</div>
|
||||
) : (
|
||||
renderInput()
|
||||
)}
|
||||
{renderInputWithIcon()}
|
||||
|
||||
{error && (
|
||||
{displayError && (
|
||||
<div id={`${inputId}-error`} className="invalid-feedback d-block" role="alert">
|
||||
{error}
|
||||
{displayError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasSuccess && (
|
||||
{displaySuccess && !displayError && (
|
||||
<div className="valid-feedback d-block">
|
||||
{success}
|
||||
{displaySuccess}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{helpText && !error && !success && (
|
||||
{helpText && !displayError && !displaySuccess && (
|
||||
<div id={`${inputId}-help`} className="form-text">
|
||||
<IconAlertCircle size={14} className="me-1" />
|
||||
{helpText}
|
||||
</div>
|
||||
)}
|
||||
@@ -144,4 +234,3 @@ function FormField({
|
||||
}
|
||||
|
||||
export default FormField;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user