Как, вроде бы, простая задача превратилась в головную боль.

Для нетерпеливых

Первое, что приходит в голову, это то, что кто‑то, скорее всего, уже сделал такой компонент, и нужно адаптировать готовое решение. При быстром поиске нашел один пример, сделанный на textarea, который и работал как обычный textarea, но с возможностью выделять слова. Плюс ответы на stackoverflow, дающие понять, что это не такая уж и тривиальная задача.

Очевидное решение — это сделать все обычным input»ом с абсолютно позиционированным div»ом на заднем фоне. Скрыть текст самого поля, оставив только каретку, и сделать видимым только содержимое заднего блока. Так я уже добился того, что у меня был полностью стилизуемый текст.

Стили включать не буду, так как это займет слишком много места. Принцип прост: задать одинаковые размеры, отступы и шрифты для div»а и input»а.

const [value, setValue] = useState('');

const onChange = (e) => setValue(e.target.value);
const format = (text) => // formatted jsx
<div className="container">
  <div className="text">{format(value)}</div>
  <input type="text" className="field" value={value} onChange={onChange} />
</div>

Первая проблема

При введении длинной строки поле начинает скроллиться, а текст в блоке стоит на своем месте.
При введении длинной строки поле начинает скроллиться, а текст в блоке стоит на своем месте.


После долгих хождений вокруг да около я с помощью коллеги пришел к тому, что можно брать значение скролла поля и ставить его заднему блоку.

// ...
const textBlockRef = useRef(null);
const [scrollValue, setScrollValue] = useState(0);
// ...
const handleScroll = (e) => setScrollValue(e.currentTarget.scrollLeft);

useEffect(() => {
  if (textBlockRef.current) {
    textBlockRef.current.scrollLeft = scrollValue;
  }
}, [scrollValue]);
<!-- ... -->
<div className="text" ref={textBlockRef}>{format(value)}</div>
<input
  type="text"
  className="field"
  value={value}
  onChange={onChange}
  onScroll={handleScroll}
 />
<!-- ... -->


Получился, вроде бы, хороший результат. Компонент работал, как и было задумано.

Вторая проблема

Вторая проблема заключается в том, что мне, оказывается, не удалось полностью избавиться от первой.

В браузере Safari не работает свойство onScroll у простого input’а.

Начал было я сомневаться в правильности своего подхода, как выяснилось, что safari поддерживает onScroll у textarea. Это решило проблему. Оставалось только стилизовать textarea, чтобы он выглядел и вел себя как простой input[type=”text”], и дело с концом.

<!-- ... -->
<textarea
  className="field"
  ref={ref}
  value={value}
  onChange={onChange}
  onScroll={handleScroll}
  rows={1}
/>
<!-- ... -->

Третья проблема (более очевидная)

При нажатии на Enter, в textarea происходил перенос строки. Это решается просто - нужно отловить нажатие и отменить его, плюс, если инпут находится в форме, сделать сабмит.

// ...
  const submitHandler = (e) => {
    if (e.keyCode === 13 || e.key === "Enter" || e.which === 13) {
      if (!e.repeat && e.target.form) {
        e.target.form.submit();
      }
      e.preventDefault();
    }
  };
// ...
<!-- ... -->
<textarea
  className="field"
  ref={ref}
  value={value}
  onChange={onChange}
  onScroll={handleScroll}
  onKeyDown={submitHandler}
  rows={1}
/>
<!-- ... -->

Так, путем нехитрых манипуляций, в течении дня, мне все-таки удалось реализовать желаемый компонент.

Финальный результат можно посмотреть на CodeSandbox.


Возможно, в этом решении найдутся еще проблемы, буду рад, если кто-то их найдет и озвучит в комментариях.

Комментарии (5)


  1. Azirel
    00.00.0000 00:00
    +1

    А можно просто не поддерживать вечно-кривой Safari и обойтись без такой магии...


  1. SunUp
    00.00.0000 00:00
    +1

    Неплохо реализовано, ещё встречал решение с вставкой после отображенного в div текста на месте курсора прозрачного input шириной с курсор, так что текст в нём никогда не виден и фокусить его по клику на div. Сходу приходит в голову ещё вариант div с contenteditable="true" и прятать input полностью, но работоспособность этой идеи нужно ещё проверить.

    Возможно, в этом решении найдутся еще проблемы


    Нашлось немного... невидимый плейсхолдер в firefox, лечится заданием для него цвета


    1. Gimir Автор
      00.00.0000 00:00

      Да, в сторону contenteditable тоже смотрел, когда встрял с проблемой на сафари, но у меня возникло ощущение что реакт с ним не дружит, да и меньше нравился такой подход из-за прямого редактирования содержимого хтмля.

      Нашлось немного... невидимый плейсхолдер в firefox, лечится заданием для него цвета

      Это мы сейчас поправим, спасибо!


  1. ebt
    00.00.0000 00:00
    +2

    Почему реакт? Ведь есть же и другие. Рекомендую писать основную логику на ванильном JS/TS и потом добавлять нужные адаптеры ????


  1. kachurun
    00.00.0000 00:00
    +1

    Это что шутка такая, зачем писать настолько уж простые статьи? Я к тому что к моменту когда я только вошёл во вкус, проблема (и статья вместе с ней) уже закончилась, собственно где тут головная боль непонятно)