拦截网页右键复制?我用HTML原生标签做了一个高亮划词翻译

2026-08-15 0 741

前几天产品提了个奇怪的需求:用户按住鼠标选择一段英文,自动在单词下面生成翻译。我寻思这不得直接上selectionchange事件加一堆JavaScript?后来做的时候发现,其实真正核心的交互,有一半是HTML原生标签撑起来的。并不是所有东西都要用

去模拟。

这篇文章不讲复杂框架,只讲怎么用HTML里的<mark><ruby><rt>和一点点原生事件,做一个纯前端的“划词翻译气泡”。HTML的语义化标签比想象中能打。

先看效果:划词出来的结果是什么

当你在网页上选中一段英文字符,鼠标松开时,选中部分会被一个半透明的 <mark> 标签包裹,并且在这个词组的下面直接显示翻译注解。注意,翻译结果是直接嵌在HTML文档流里的,而不是弹出绝对定位的小窗。

这样做的好处是:翻译和原文在一起,翻完就能对照,而且不影响页面滚动。坏处是改变了原本文本结构,所以我会在用户点击别处时把翻译移除。

第一步:先做出一个可以重复使用的HTML结构

真正实用的翻译标注长这样:

        <mark id="trans-zone">
            <span class="original">apple</span>
            <ruby>
                <rt>苹果</rt>
            </ruby>
        </mark>
    

这里的 <mark> 本身就是用来高亮文本的,语义上完全契合。而 <ruby> 标签专门用来做注音或注释,里面的 <rt> 就是显示在文字上方的注释内容。平时用它显示拼音,现在拿来放中文翻译,毫无违和感。

为什么不用然后自己写样式?因为没必要。mark标签自带高亮背景,ruby自带排版位置,省去一堆手写样式。而且屏幕阅读器对这两个标签的处理都很成熟,可访问性反而更好。

第二步:监听“选区变化”和“鼠标松开”

真正划词的效果是实时预判。我监听的是mouseup事件,而不是selectionchange。因为只有鼠标松开了,用户才算完成一次选择。

        document.addEventListener('mouseup', function(e) {
            // 先清除上一次的翻译
            removePrevMark();

            // 获取当前的选区
            const selection = window.getSelection();
            if (!selection || selection.isCollapsed) return;

            // 只处理纯英文的简单情况(防止点到按钮时误触发)
            const selectedText = selection.toString().trim();
            if (!/^[a-zA-Zs]+$/.test(selectedText)) return;

            // 真正去生成翻译标记
            translateSelection(selection);
        });
    

这样一来,用户只要在正文里选中英文,松开鼠标就会触发生成。为了防止点到按钮或者输入框时也触发,我额外判断了e.target.closest('button, input, textarea, [contenteditable]')是否能匹配到,是的话就跳过。

第三步:把选中的文字替换成mark结构

这一步是核心,也是最麻烦的。因为不能用innerHTML直接改body,那会把选区所在的整段文字都打乱,必须精确地替换选中范围。

好在浏览器提供range.deleteContents()range.insertNode(),可以原地替换DOM节点。

        function translateSelection(selection) {
            const range = selection.getRangeAt(0);
            const selectedText = selection.toString().trim();

            // 把选中内容替换成一个mark节点
            const mark = document.createElement('mark');
            const originalSpan = document.createElement('span');
            originalSpan.textContent = selectedText;

            // 翻译直接使用一个简单的词典对象
            const dict = { apple: '苹果', banana: '香蕉', 'hello world': '你好世界' };
            const lower = selectedText.toLowerCase();
            const translated = dict[lower] || '未收录';

            const ruby = document.createElement('ruby');
            const rt = document.createElement('rt');
            rt.textContent = translated;
            ruby.appendChild(rt);

            mark.appendChild(originalSpan);
            mark.appendChild(ruby);

            // 范围清空,插入新节点
            range.deleteContents();
            range.insertNode(mark);

            // 移除选区,避免继续选中mark里的内容
            selection.removeAllRanges();
        }
    

这里为了演示,我硬编码了一个小字典。实际项目中可以改成异步请求翻译API,拿到结果之后再塞进rt标签里。甚至可以在请求返回之前先显示“加载中”,通过回调更新rt.textContent

第四步:如何把翻译标记“不留痕迹”地清掉

总不能翻译完就一直留在那里。我设置了两种清除时机:一是点击页面其他任意地方,二是按下Esc键。清理的逻辑就是把mark里的原始文字回填到原来的位置。

        function removePrevMark() {
            const mark = document.querySelector('mark');
            if (!mark) return;

            // 取出原始文本
            const original = mark.querySelector('span.original') || mark.firstChild;
            const textNode = document.createTextNode(original.textContent);

            // 用文本节点替换mark节点
            mark.parentNode.replaceChild(textNode, mark);

            // 清除所有高亮痕迹
            const allMarks = document.querySelectorAll('mark');
            allMarks.forEach(m => {
                const txt = m.textContent;
                const text = document.createTextNode(txt);
                m.parentNode.replaceChild(text, m);
            });
        }

        document.addEventListener('click', function(e) {
            // 防止点击的是翻译内容本身时马上消失
            if (e.target.closest('mark')) return;
            removePrevMark();
        });

        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') removePrevMark();
        });
    

注意,如果把mark内的ruby也一并替换掉,会丢失原始单词,所以我在创建mark时就给原始文本包了一个<span class="original">,清掉时只取里面的文字。

第五步:遇到跨节点选中怎么办?

上面的简单实现只适用于选中纯文本节点。如果你跨了多个标签,比如在<span>Hello</span><span>World</span>里面选,deleteContents会把这些节点一起删掉,然后插入一个mark。这会导致原本的<span>样式丢失。

我对这个问题的处理方式简单粗暴:只处理同一段落内文本节点连续的选区。更复杂的跨行直接不支持。在调用translateSelection之前先判断:

        const commonAncestor = range.commonAncestorContainer;
        if (commonAncestor.nodeType !== Node.TEXT_NODE && commonAncestor.children.length > 10) return;
    

一般文章正文里的划词基本都在一个段落内,所以够用了。

进一步优化:延迟翻译避免鼠标误选

有时候用户只是快速选择一下没选好,导致每次都发起翻译,多少影响体验。我后来加了一个300ms的防抖,只有选中完毕后停留超过300ms才真正替换DOM。如果在300ms内又鼠标按下去,就取消。

这个防抖很简单,代码里用一个全局变量存定时器id:

        let timer = null;
        document.addEventListener('mouseup', function(e) {
            clearTimeout(timer);
            if (e.target.closest('button')) return;
            const selection = window.getSelection();
            if (!selection || selection.isCollapsed) return;

            timer = setTimeout(() => {
                // 在定时器里再拿一次selection,防止选区已经变化
                const latestSel = window.getSelection();
                if (!latestSel.toString().trim()) return;
                translateSelection(latestSel);
            }, 300);
        });
    

这样用户体验会好很多,不会因为手一抖就闪一下高亮。

关于样式:实在忍不住写了一点内联样式怎么办?

题目说不要样式,但我自己用的时候还是给mark加了个高亮背景。最简单的办法是用浏览器默认的mark样式,那玩意儿本身就是黄色高亮,不用写任何CSS。如果你想换个颜色,我建议直接放在CSS文件里,比如mark { background: #ffe68f; },但我这里就不加style标签了。

值得注意的是,<ruby><rt>默认字体非常小,而且紧贴在上方。如果你希望它显示得更“像翻译”,可以给它设置font-size:0.8em,但这些都交给外部样式表,不在这次HTML里写死。

踩坑记:原生的mark标签会参与滚动选区的尺寸计算

替换完成之后,你会发现选区自动消失了,文本也会轻微跳动,因为mark的背景色和rt的高度会让整段文字稍微变高。解决方案是提前给mark设置一个固定的行高,或者用vertical-align让rt不要影响行盒。不过这些属于样式范畴,我实际使用中直接把mark的display设置成inline,然后ruby里rt的line-height设为0,这样就不会撑高段落。

最后一点想法:不是所有交互都需要一堆JavaScript

这次用到的HTML标签其实早就存在,markruby都是非常冷门的语义标签。但结合起来做“划词翻译”意外顺手。以前觉得HTML标签就那些常用的,但真去了解才发现,很多冷门标签反而能做出现代前端需要反复模拟的效果。

就算产品最终没上线这个功能,这段代码也让我搞懂了Range对象和原生标签的配合方式。如果你也遇到类似的需求,别只想着“用JS操作一块白板”,先回头翻翻HTML的标签库,也许早就给你备好现成的了。

拦截网页右键复制?我用HTML原生标签做了一个高亮划词翻译
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 html 拦截网页右键复制?我用HTML原生标签做了一个高亮划词翻译 https://www.taomawang.com/web/html/2548.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务