javascript - 如何在只知道它的索引的情况下获得段落内单词的边界矩形?

标签 javascript html css reactjs text-to-speech

我正在 React 应用程序中创建文本转语音功能,该功能可以通过在当前说出的单词后面放置背景来突出显示当前说出的单词。

该功能与 Firefox reader view 非常相似.

我实现的解决方案只是剪切段落字符串,并在每次渲染时在口语单词周围放置一个跨度,这会占用大量资源并且无法制作动画。

这是代码:(我打算废弃)

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,然后是 moved around :

Single word highlight

包含高亮效果的div直接使用绝对坐标放置。

他们如何在只知道字符串索引的情况下访问段落内单词的边界矩形?

最佳答案

Here is the result of the following solution


编辑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());

渲染效果:

{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>
    );
}

移动 div 的 CSS :

.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;
}

如果有人感兴趣,我将上传该解决方案的视频

关于javascript - 如何在只知道它的索引的情况下获得段落内单词的边界矩形?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61290220/

相关文章:

css - Bootstrap 自定义断点

html - 如何删除最后一个 child 的边框?

javascript - 如何在鼠标悬停时更改 setInterval() 回调函数不断修改的元素的值?

javascript - react : generating input field data with a button component

php - 确认删除不起作用

HTML 不寻常的 flex 包装

java - 你可以将 html 彩色文本添加到包含共享首选项的字符串中吗?

javascript - Twitter Bootstrap 风格 = "width: 45%;"

javascript - Websocket - WebSocket 握手期间出错 : Unexpected response code: 404

javascript - Firefox 中的奇怪时区 "Etc/GMT-1"