Вот результат следующего решения
РЕДАКТИРОВАТЬ 2:
Как указано в комментариях, с фиксированным позиционирование приведет к проблеме, когда экран изменяет размеры и когда пользователь увеличивает или прокручивает.
Чтобы создать относительное позиционирование, можно сначала получить смещения родительского элемента: const { offsetTop, offsetLeft } = containerEl.current;
А затем вычтите их в выборку DomRect:
return Array.from(range.getClientRects()).map(
({ top, left, width, height }) => ({
top: top - offsetTop,
left: left - offsetLeft,
width,
height,
})
);
Просто примените position: relative
к родительскому тексту, а затем position: absolute
к наложению текста и вуаля.
РЕДАКТИРОВАТЬ:
Решение ниже не будет работать с завернутым словом (например, non-violent
на рисунке ниже)
![enter image description here](https://i.stack.imgur.com/iSeaL.png)
Получившийся прямоугольник занимает прямоугольник, покрывающий обе части слова.
Вместо этого используйте getClientRects
, чтобы получить все поля, где отображается одна и та же строка, затем сопоставьте ее с тем же наложением:
Тип состояния: const [highlighst, setHighlights] = useState<DOMRect[] | null>(null);
В настройках выделения: return Array.from(range.getBoundingClientRect());
The r endering:
{highlights &&
highlights.map(({ top, left, width, height }) => (
<span
className='text-highlight'
style={{
top,
left,
width,
height,
}}
></span>
))}
Результат:
![enter image description here](https://i.stack.imgur.com/Jbf48.png)
В итоге я смог сделать это, используя Range API .
Методы setStart
и setEnd
могут принимать переменную индекса в качестве второго параметра.
Затем я получаю текстовые координаты используя getBoundingClientRect
для самого диапазона и поместив его в мое состояние.
Теперь я могу применить эти значения к фиксированному div в моем рендере:
const range = document.createRange();
export default function TextNode({ content, footnote }: TextNodeProps) {
const [highlight, setHighlight] = useState<DOMRect | null>(null);
const containerEl = useRef<HTMLSpanElement>(null);
useEffect(() => {
registerText((ev) => {
if (!ev) {
setHighlight(null);
return;
}
if (ev.type === 'sentence') {
(textEl.current as HTMLSpanElement | null)?.scrollIntoView(
scrollOptions
);
}
if (ev.type === 'word')
setHighlight((old) => {
const txtNode = containerEl.current?.firstChild as Node;
range.setStart(txtNode, ev.start);
range.setEnd(txtNode, ev.end);
if (!old) {
(containerEl.current as HTMLSpanElement | null)?.scrollIntoView(
scrollOptions
);
}
return range.getBoundingClientRect();
});
}, content);
}, [content]);
return (
<span ref={containerEl}>
{content}
{highlight && (
<div
className='text-highlight'
style={{
top: highlight.top,
left: highlight.left,
width: highlight.width,
height: highlight.height,
}}
></div>
)}
</span>
);
}
CSS для движущегося div:
.text-highlight {
position: fixed;
border-bottom: 4px solid blue;
opacity: 0.7;
transition-property: top, left, height, width;
transition-duration: 0.2s;
transform-style: ease-in-out;
}
Если кому-то будет интересно, я выложу видео решения, работающего