Как, вроде бы, простая задача превратилась в головную боль.
Первое, что приходит в голову, это то, что кто‑то, скорее всего, уже сделал такой компонент, и нужно адаптировать готовое решение. При быстром поиске нашел один пример, сделанный на 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)
SunUp
00.00.0000 00:00+1Неплохо реализовано, ещё встречал решение с вставкой после отображенного в div текста на месте курсора прозрачного input шириной с курсор, так что текст в нём никогда не виден и фокусить его по клику на div. Сходу приходит в голову ещё вариант div с contenteditable="true" и прятать input полностью, но работоспособность этой идеи нужно ещё проверить.
Возможно, в этом решении найдутся еще проблемы
Нашлось немного... невидимый плейсхолдер в firefox, лечится заданием для него цветаGimir Автор
00.00.0000 00:00Да, в сторону contenteditable тоже смотрел, когда встрял с проблемой на сафари, но у меня возникло ощущение что реакт с ним не дружит, да и меньше нравился такой подход из-за прямого редактирования содержимого хтмля.
Нашлось немного... невидимый плейсхолдер в firefox, лечится заданием для него цвета
Это мы сейчас поправим, спасибо!
ebt
00.00.0000 00:00+2Почему реакт? Ведь есть же и другие. Рекомендую писать основную логику на ванильном JS/TS и потом добавлять нужные адаптеры ????
kachurun
00.00.0000 00:00+1Это что шутка такая, зачем писать настолько уж простые статьи? Я к тому что к моменту когда я только вошёл во вкус, проблема (и статья вместе с ней) уже закончилась, собственно где тут головная боль непонятно)
Azirel
А можно просто не поддерживать вечно-кривой Safari и обойтись без такой магии...