Как я могу получить ограничивающий прямоугольник слова в абзаце, зная только его индекс? - PullRequest
2 голосов
/ 18 апреля 2020

Я создаю функцию преобразования текста в речь в приложении реагирования, которая сможет выделить текущее произнесенное слово, поместив фон за ним.

Функция очень похожа на Firefox представление читателя .

Решение, которое я реализовал, просто обрезает строку абзаца и помещает интервал вокруг произнесенного слова при каждом рендеринге, делая его тяжелым по ресурсам и невозможным для анимации.

Вот код: (который я собираюсь отменить)

export interface SpeakEvent {
    start: number;
    end: number;
    type: string;
}

export default function TextNode({ content }: TextNodeProps) {
    const [highlight, setHighlight] = useState<SpeakEvent | null>(null);

    useEffect(() => {
        registerText((ev) => {
            if (ev?.type === 'word' || !ev)
                setHighlight((old) => {
                    /* Irrelevant code */
                    return ev;
                });
        }, content);
    }, [content]);

    const { start, end } = highlight ?? {};

    let segments = [content];

    if (highlight) {
        segments = [
            segments[0].slice(0, start),
            segments[0].slice(start, end),
            segments[0].slice(end),
        ];
    }

    return (
        <>
            {segments.map((seg, i) =>
                i === 1 ? (
                    <span key={i} className={'highlight'}>
                        {seg}
                    </span>
                ) : (
                    seg
                )
            )}
        </>
    );
}

Считыватель Firefox использует более разумный способ сделать это. Он использует div, помещенный за произнесенным словом, который затем перемещается вокруг :

Single word highlight

Div, содержащий эффект выделения, напрямую помещается с использованием абсолютных координат.

Как они могут получить доступ к ограничивающему прямоугольнику слова в абзаце, зная только индекс строки?

1 Ответ

1 голос
/ 18 апреля 2020

Вот результат следующего решения


РЕДАКТИРОВАТЬ 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

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

Вместо этого используйте 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


В итоге я смог сделать это, используя 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;
}

Если кому-то будет интересно, я выложу видео решения, работающего

...