自定义右键菜单:在项目里实现“选中文字即刻生成新提示”

一个真正丝滑的项目,交互不能只停留在点击按钮。 "选中即触发" (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 会让浮窗飞到九霄云外。

正确的绝对定位公式:

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) Left=rect.left+window.scrollX+(rect.width/2)−(menuWidth/2)

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 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)。

相关推荐
不要试图纠正别人6 小时前
别让大模型算钱:我把优惠券组合优化写成了确定性算法
前端
志尊宝6 小时前
Vue3 零基础每日笔记(075):图片懒加载与骨架屏
javascript·vue.js·笔记
IT_Octopus6 小时前
【零基础入门 LLM 开发 · Day 11】:ChatOpenAI vs init_chat_model——一行换供应商
java·前端·javascript
柚yuzumi6 小时前
React 19 Hooks 入门:函数组件的四大法宝 (useState / useEffect / useRef / useContext)
前端·javascript·架构
deli0076 小时前
撒下 200 个种子点,平面为什么自己长成蜂窝?Voronoi 图实验室
前端
King禅7 小时前
JavaScript 调试实战指南:从 console.log 到 DevTools 深度运用
javascript
大文说跨境7 小时前
多账号环境隔离方案技术选型:指纹浏览器、VPS 与云手机的三种架构对比
java·开发语言·前端
咕白m6257 小时前
使用 C# 将 TIFF 转换为 PDF
前端·c#
全栈项目管理程序猿7 小时前
ArcGIS JS 基础教程(28):图层渲染顺序管理
前端·javascript
huakoh7 小时前
MCP 工具报错走哪条通道:三条探针的最小复现检查
前端