Загрузка данных


import { yupResolver } from '@hookform/resolvers/yup';
import {
  Column,
  FieldHeader,
  Row,
  TextField,
  TextFieldProps,
  Typography,
} from '@shared/components';
import { ipInputSchema } from '@shared/components/IpInput/validator';
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

type Props = {
  'data-fui-tid'?: string;
  value?: string;
  onChange: (value: string) => void;
  error: boolean;
  helperText: string | undefined;
  label: string;
  required?: boolean;
  disabled?: boolean;
  onBlur?: VoidFunction;
};

type FormValues = {
  first: string;
  second: string;
  third: string;
  fourth: string;
};

type FieldName = keyof FormValues;

const NEXT_FIELD_MAP: Record<FieldName, FieldName | undefined> = {
  first: 'second',
  second: 'third',
  third: 'fourth',
  fourth: undefined,
};

const PREVIOUS_FIELD_MAP: Record<FieldName, FieldName | undefined> = {
  first: undefined,
  second: 'first',
  third: 'second',
  fourth: 'third',
};

const MAX_IP_OCTET = 255;

export const IpInput = ({
  'data-fui-tid': tid,
  value: outerValue,
  onChange: onIpChange,
  error,
  helperText,
  label,
  required,
  onBlur,
  disabled,
}: Props) => {
  const { t } = useTranslation('controller');

  const ipFields: string[] = outerValue
    ? outerValue.split('.')
    : ['', '', '', ''];

  const defaultValues: FormValues = {
    first: ipFields[0],
    second: ipFields[1],
    third: ipFields[2],
    fourth: ipFields[3],
  };

  const { control, setFocus, reset, getValues, setValue } = useForm<FormValues>(
    {
      mode: 'onChange',
      defaultValues,
      resolver: yupResolver(ipInputSchema),
    },
  );

  useEffect(() => {
    reset(defaultValues);
  }, [outerValue]);

  const moveToNextField = (currentField: FieldName) => {
    const nextField = NEXT_FIELD_MAP[currentField];
    if (nextField) {
      setFocus(nextField);
    }
  };

  const moveToPreviousField = (currentField: FieldName) => {
    const previousField = PREVIOUS_FIELD_MAP[currentField];
    if (previousField) {
      setFocus(previousField);
    }
  };

  const updateIpValue = () => {
    const ipFields = Object.values(getValues()).join('.');
    const ipValue = ipFields === '...' ? '' : ipFields;
    onIpChange(ipValue);
    handleBlur();
  };

  const handleCopy = () => navigator.clipboard.writeText(outerValue ?? '');

  const handlePaste: TextFieldProps['onPaste'] = (event) => {
    event.preventDefault();

    const value = event.clipboardData.getData('text/plain');

    const parts = value?.split('.') ?? [];
    const parsedValue = parts.map((field) => field.replace(/\r\n|\n\r/gm, ''));

    const isValid =
      parsedValue.length === 4 &&
      parsedValue.every((part) => /^[\d.]+$/.test(part));

    const isWithinRange = parsedValue.every((part) => {
      const num = parseInt(part, 10);
      return num >= 0 && num <= 255;
    });

    if (!isValid || !isWithinRange) {
      console.warn('Invalid IP format, paste cancelled');
      return;
    }

    onIpChange(value);

    reset({
      first: parsedValue[0],
      second: parsedValue[1],
      third: parsedValue[2],
      fourth: parsedValue[3],
    });
  };

  const handleBackspace = (fieldName: FieldName, currentValue: string) => {
    if (currentValue === '' && fieldName !== 'first') {
      const previousField = PREVIOUS_FIELD_MAP[fieldName];
      if (previousField) {
        const previousValue = getValues()[previousField];
        const newPreviousValue = previousValue.slice(0, -1);

        setValue(previousField, newPreviousValue);
        moveToPreviousField(fieldName);

        setTimeout(updateIpValue, 0);
        return true;
      }
    }
    return false;
  };

  const handleExceedLimit = (
    fieldName: FieldName,
    inputValue: string,
    onChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
  ) => {
    // Если это последнее поле, ограничиваем до 255
    if (fieldName === 'fourth') {
      onChange({
        target: { value: String(MAX_IP_OCTET) },
      } as React.ChangeEvent<HTMLInputElement>);
      setTimeout(updateIpValue, 0);
      return true;
    }

    // Находим максимальную часть, которая не превышает 255
    let validPart = '';
    let remainingPart = inputValue;

    for (let i = 1; i <= inputValue.length; i++) {
      const testValue = inputValue.slice(0, i);
      if (parseInt(testValue, 10) <= MAX_IP_OCTET) {
        validPart = testValue;
        remainingPart = inputValue.slice(i);
      } else {
        break;
      }
    }

    // Обновляем текущее поле (validPart всегда непустой при вызове функции)
    onChange({
      target: { value: validPart },
    } as React.ChangeEvent<HTMLInputElement>);

    // Переносим остаток в следующее поле
    if (remainingPart) {
      const nextField = NEXT_FIELD_MAP[fieldName];
      if (nextField) {
        const nextFieldValue = getValues()[nextField];
        setValue(nextField, remainingPart + nextFieldValue);
      }
    }

    // Перемещаем фокус на следующее поле
    moveToNextField(fieldName);
    setTimeout(updateIpValue, 0);

    return true;
  };

  const handleChange =
    (
      fieldName: FieldName,
      onChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
    ): TextFieldProps['onChange'] =>
    (event) => {
      // Проверяем тип события и пропускаем вставку из буфера обмена
      if (
        event.nativeEvent instanceof InputEvent &&
        event.nativeEvent.inputType === 'insertFromPaste'
      ) {
        return;
      }

      let { value } = event.target;

      // Очищаем значение от нецифровых символов
      value = value.replace(/\D/g, '');

      // Проверяем, не превышает ли число 255
      if (value && parseInt(value, 10) > MAX_IP_OCTET) {
        const handled = handleExceedLimit(fieldName, value, onChange);
        if (handled) return;
      }

      // Обновляем значение в поле
      onChange({ target: { value } } as React.ChangeEvent<HTMLInputElement>);

      // Если значение достигло 3 символов и это не последнее поле, перемещаем фокус
      if (value.length === 3 && fieldName !== 'fourth') {
        moveToNextField(fieldName);
      }

      // Формируем полный IP адрес
      setTimeout(updateIpValue, 0);
    };

  const handleKeyDown =
    (fieldName: FieldName, currentValue: string): TextFieldProps['onKeyDown'] =>
    (event) => {
      if (event.key === 'Backspace') {
        event.preventDefault();
        const handled = handleBackspace(fieldName, currentValue);
        if (!handled) {
          // Если backspace не обработан, имитируем обычное удаление
          const newValue = currentValue.slice(0, -1);
          setValue(fieldName, newValue);
          setTimeout(updateIpValue, 0);
        }
      }
    };

  const handleBlur = () => onBlur && onBlur();

  const renderTextField = (name: FieldName, testId: string) => (
    <Controller
      name={name}
      control={control}
      render={({ field: { value, onChange, ref } }) => (
        <TextField
          inputRef={ref}
          value={value}
          onChange={handleChange(name, onChange)}
          onKeyDown={handleKeyDown(name, value)}
          onBlur={handleBlur}
          onPaste={handlePaste}
          onCopy={handleCopy}
          data-testid={testId}
          error={error}
          disabled={disabled}
        />
      )}
    />
  );

  return (
    <Column gridGap='0' data-fui-tid={tid} data-testid='ip-input'>
      <FieldHeader label={t(label)} required={required} disabled={disabled} />
      <Row gridGap='2px'>
        {renderTextField('first', 'ip-first')}
        <span style={{ display: 'inline-block', alignSelf: 'end' }}>.</span>
        {renderTextField('second', 'ip-second')}
        <span style={{ display: 'inline-block', alignSelf: 'end' }}>.</span>
        {renderTextField('third', 'ip-third')}
        <span style={{ display: 'inline-block', alignSelf: 'end' }}>.</span>
        {renderTextField('fourth', 'ip-fourth')}
      </Row>
      {error && (
        <Typography
          variant='caption'
          color='error'
          style={{ marginTop: '4px' }}
        >
          {helperText}
        </Typography>
      )}
    </Column>
  );
};


  const handleExceedLimit = (
    fieldName: FieldName,
    inputValue: string,
    onChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
  ) => {
Refactor this function to not always return the same value.
    // Если это последнее поле, ограничиваем до 255
    if (fieldName === 'fourth') {
      onChange({
        target: { value: String(MAX_IP_OCTET) },
      } as React.ChangeEvent<HTMLInputElement>);
      setTimeout(updateIpValue, 0);
      return true;
    }
    // Находим максимальную часть, которая не превышает 255
    let validPart = '';
    let remainingPart = inputValue;


    // Перемещаем фокус на следующее поле
    moveToNextField(fieldName);
    setTimeout(updateIpValue, 0);
    return true;
  };
  const handleChange =
    (
      fieldName: FieldName,