Текстовый Инпут с возможностью выделять отдельные слова

от автора

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

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


Первое, что приходит в голову, это то, что кто‑то, скорее всего, уже сделал такой компонент, и нужно адаптировать готовое решение. При быстром поиске нашел один пример, сделанный на 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.


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


ссылка на оригинал статьи https://habr.com/ru/post/721322/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *