Skip to content

feat(text-selection): 选中文字弹出引用/复制浮窗#149

Open
SsparKluo wants to merge 1 commit into
lehhair:mainfrom
SsparKluo:feat/text-selection-floating-popup
Open

feat(text-selection): 选中文字弹出引用/复制浮窗#149
SsparKluo wants to merge 1 commit into
lehhair:mainfrom
SsparKluo:feat/text-selection-floating-popup

Conversation

@SsparKluo

@SsparKluo SsparKluo commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

概述

在聊天消息区域选中文字后,弹出悬浮工具栏,提供「引用」和「复制」两个操作。

  • 引用:将选中的文字转为 Markdown 引用块(> ),插入到当前对话面板的输入框光标位置,并自动聚焦输入框
  • 复制:将选中的文字复制到剪贴板,按钮短暂显示「已复制」反馈

实现细节

交互流程

  1. selectionchange 事件实时追踪选区状态到 ref,但不显示浮窗
  2. mouseup / touchend / 键盘 Shift+方向键 释放时才提交显示——避免拖选过程中浮窗闪烁
  3. 浮窗定位在鼠标释放点附近(而非选区起始端),模仿浏览器原生选区工具栏的行为
  4. 点击外部、滚动、缩放窗口、按 Esc 均自动关闭浮窗

定位策略

  • 鼠标/触摸:使用 pointerup 的 clientX/Y 作为锚点,浮窗显示在指针上方
  • 键盘选择:回退到选区的 bounding rect
  • 水平方向自动 clamp 到视口范围内,垂直方向在空间不足时自动翻转到下方

Pane 定位

ChatPane 根元素上添加了 data-pane-id 属性。这是必要的——消息 DOM 和输入框是 ChatPane 内部的兄弟子树,之前 data-pane-id 只挂在 InputBox 容器上,导致从消息文字向上 closest('[data-pane-id]') 找不到 pane 祖先。现在两者共享同一个 data-pane-id 祖先,split 模式下也能正确路由到对应 pane 的输入框。

过滤规则

以下场景不弹出浮窗:

  • 选区折叠(无实际选中内容)
  • 选区在 inputtextarea[contenteditable="true"]
  • 选区在 [data-no-selection-popup] 标记的区域内
  • 选区不在任何 data-pane-id 容器内(如侧边栏、终端)

引用块格式化

  • 每行加 > 前缀,空行变为单独的 >,保持 Markdown blockquote 语义
  • 插入位置前后智能补 \n,确保引用块始终独占一段(对齐 Slack / Notion 行为)

技术实现

  • 组件挂在 App 根级,通过 createPortal 渲染到 document.bodyposition: fixed 定位不受父级 transform / overflow 影响
  • Quote 操作通过 textarea 的原生 value setter + dispatchEvent(new Event('input')) 驱动,React 受控组件正常同步
  • QuoteIcon 复用 lucide-react 已有的 Quote 图标,通过 Icons.tsx barrel 统一导出
  • i18n 覆盖 enzh-CN

文件变更

文件 说明
src/features/text-selection-popup/ 新 feature 目录(组件 + 工具函数 + 测试)
src/App.tsx 挂载 <TextSelectionPopup />
src/features/chat/ChatPane.tsx 根元素加 data-pane-id
src/components/Icons.tsx QuoteIcon
src/locales/{en,zh-CN}/chat.json textSelectionPopup i18n keys

测试

  • 34 个新增测试(工具函数 + 组件行为)
  • 全项目 607 个测试全部通过
  • typecheck / lint / build 全绿

截图

image

Surface a floating popup with Quote and Copy buttons when the user
selects text inside a chat pane. Quote turns the selection into a
Markdown blockquote and inserts it into the pane's input box at the
cursor; Copy writes the raw selection to the clipboard.

Architecture:
- Mounted once in App, renders into document.body via createPortal
- selectionchange tracks a pending ref; popup only commits on
  mouseup/touchend/Shift+arrow keyup — never flickers mid-drag
- Positions near the pointer release point (mouse/touch) or the
  selection rect (keyboard)
- Auto-dismisses on outside pointerdown, scroll, resize, Esc
- data-pane-id added to ChatPane root so message selections resolve
  to the correct pane's textarea across split panes

Filters out selections inside form controls, contenteditable, and
[data-no-selection-popup] opt-out zones. Quote spacing follows
Slack/Notion conventions (always at least one blank line around the
block).

607 tests pass (34 in this feature). 0 lint warnings.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant