一个真正丝滑的项目,交互不能只停留在点击按钮。 "选中即触发" (Selection-to-Action)是生产力工具的标配。
实现这个功能看似简单,实则藏着不少关于 Selection API 和视口坐标计算的深坑。
1. 核心流程:监听、获取、定位
第一步:捕捉用户的"选中时刻"
虽然有 selectionchange 事件,但在做浮窗时,mouseup 通常更稳,因为它能确保是在用户完成拖拽动作后触发。
JavaScript
ini
document.addEventListener('mouseup', handleSelection);
function handleSelection(e) {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText.length > 0) {
const range = selection.getRangeAt(0);
// 关键点:获取选中文字在视口中的精确几何位置
const rect = range.getBoundingClientRect();
showFloatingMenu(rect, selectedText);
} else {
hideFloatingMenu();
}
}
2. 坐标计算:让浮窗"如影随形"
这是最容易翻车的地方。getBoundingClientRect() 返回的是相对于**视口(Viewport)**的坐标。如果你的页面有滚动条,或者容器是 position: relative,直接赋值 top/left 会让浮窗飞到九霄云外。
正确的绝对定位公式:
<math xmlns="http://www.w3.org/1998/Math/MathML"> L e f t = r e c t . l e f t + w i n d o w . s c r o l l X + ( r e c t . w i d t h / 2 ) − ( m e n u W i d t h / 2 ) Left = rect.left + window.scrollX + (rect.width / 2) - (menuWidth / 2) </math>Left=rect.left+window.scrollX+(rect.width/2)−(menuWidth/2)
<math xmlns="http://www.w3.org/1998/Math/MathML"> T o p = r e c t . t o p + w i n d o w . s c r o l l Y − m e n u H e i g h t − o f f s e t Top = rect.top + window.scrollY - menuHeight - offset </math>Top=rect.top+window.scrollY−menuHeight−offset
JavaScript
javascript
function showFloatingMenu(rect, text) {
const menu = document.getElementById('floating-menu');
const offset = 10; // 距离文字上方的间距
// 计算位置:居中显示在选中文字上方
const left = rect.left + window.scrollX + (rect.width / 2);
const top = rect.top + window.scrollY - offset;
Object.assign(menu.style, {
display: 'flex',
left: `${left}px`,
top: `${top}px`,
transform: 'translate(-50%, -100%)' // 利用 transform 实现水平对齐
});
menu.dataset.selectedText = text; // 暂存文字供后续使用
}
3. 避坑指南
① 避免"点一下就弹"
如果用户只是单纯点击了一下(没有选中任何字),mouseup 也会触发。
- 解决 :除了判断
selectedText.length > 0,还可以记录mousedown的位置,如果mouseup的位置没变,说明是点击而非选择。
② 浮窗点击冲突:onmousedown 陷阱
当你点击浮窗上的"复制"按钮时,浏览器默认会清除当前页面的文字选中状态,导致 mouseup 再次触发把浮窗关掉。
- 解决 :在浮窗容器上使用
onmousedown={(e) => e.preventDefault()}。这样点击浮窗时,焦点不会离开原来的文字。
③ 边界检测(Viewport Boundary)
如果选中的文字在屏幕最顶端,浮窗会超出屏幕。
- 对策 :判断
rect.top是否小于浮窗高度。如果是,则将浮窗显示在文字下方。
4. 功能实现:一键复制与翻译
JavaScript
ini
// 复制逻辑(复用我们上一篇文中的 safeCopy)
menu.querySelector('.copy-btn').onclick = async () => {
const text = menu.dataset.selectedText;
await safeCopy(text);
showToast('已复制!');
hideFloatingMenu();
};
// 翻译逻辑:调用 AI 接口
menu.querySelector('.translate-btn').onclick = async () => {
const text = menu.dataset.selectedText;
// 直接跳转到 AI 对话框并自动输入 Prompt
router.push(`/chat?prompt=请翻译以下文字:${text}`);
};
5. 交互进阶:移动端长按适配
在移动端,用户习惯长按选择文字。
- 方案 :现代移动浏览器会自动弹出系统菜单。如果你想覆盖它,需要监听
contextmenu事件,或者通过CSS属性-webkit-touch-callout: none;禁用系统菜单,再手写一套长按逻辑(touchstart+setTimeout)。