SelectionActions 划词工具条
浮在选区下方的工具条,把选中的文字交给智能体改写。
SelectionActions 划词工具条
基础用法
划词改写
选中段落里的句子,工具条浮现在选区最后一行的下方。四个状态:idle → thinking → streaming → result。
示例加载中...
两层:追踪与呈现
组件本身只负责呈现:给它一个 selection 快照和一个 state,它负责定位、形变与动作行。选区从哪来是宿主的事。
useSelectionAnchor() 是配套的追踪层,基于 @vueuse/core 的 useTextSelection,适用于普通文档正文。contenteditable、虚拟列表、iframe 这类场景自己构造 SelectionPayload 喂进来即可。
import { resolveSelectionPayload, useSelectionAnchor } from '@talex-touch/tuffex/selection-actions'
const { selection, clear } = useSelectionAnchor({
root: articleRef, // 只追踪这棵子树里的选区
debounce: 120, // selectionchange 每帧都触发,必须收敛
minLength: 1, // 太短的选区不值得弹条
ignore: () => [barEl], // 焦点落在工具条上不算「取消选中」
})
resolveSelectionPayload() 是它内部那条纯函数规则,单独导出以便测试或自定义追踪。
useSelectionAnchor与resolveSelectionPayload是运行时函数,不是组件。TxSelectionActions作为全局标签可以直接写在模板里,但这两个必须像上面那样从@talex-touch/tuffex/selection-actions显式import——它们不在全局组件注册表里。
useSelectionAnchor 选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
root | Element | null | — | 限定追踪范围,不传则监听整篇文档。 |
debounce | number | 120 | selectionchange 的收敛时间(ms)。拖选时它每帧都触发。 |
minLength | number | 1 | 去空白后的最小长度。 |
disabled | boolean | false | 暂停上报而不卸载。 |
ignore | Element[] | [] | 焦点落进这些元素时不视为取消选中。 |
返回 { selection, clear }:selection 是 Ref<SelectionPayload | null>,clear() 在改写落地或放弃后调用。
API
Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
selection | SelectionPayload | null | null | { text, rects, range? }。为空即收起工具条。 |
state | 'idle' | 'thinking' | 'streaming' | 'result' | 'idle' | 状态机由宿主持有,组件不发请求。 |
actions | SelectionActionItem[] | Explain / Improve + 折叠的 Shorten / Tone / Grammar | 动作列表:{ id, label, more?, busyLabel? }。 |
activeActionId | string | — | 正在运行的动作 id,决定忙碌文案。 |
expanded | boolean | — | v-model:expanded,折叠动作组。 |
prompt | string | — | v-model:prompt,自由指令输入。 |
hidePrompt | boolean | false | 隐藏输入框与发送键。 |
placeholder | string | 'Describe edits' | 输入框占位,同时用作它的无障碍名。 |
ariaLabel | string | 'Selection actions' | 工具条的无障碍名。 |
keepLabel / discardLabel | string | 'Keep' / 'Discard' | 结果态两颗按钮。 |
retryLabel | string | 'Try again' | 重试按钮的无障碍名。 |
sendLabel | string | 'Send edit instruction' | 发送键的无障碍名。 |
expandLabel / collapseLabel | string | 'Show more actions' / 'Show fewer actions' | chevron 的无障碍名。 |
busyLabel | string | 'Editing' | 动作没写 busyLabel 时的兜底忙碌文案。 |
offset | number | 8 | 距选区最后一行的距离(px)。 |
Events
| 事件名 | 参数 | 说明 |
|---|---|---|
action | ({ id, action, selection }) | 按下某个预设动作,携带选区快照。 |
submit | ({ prompt, selection }) | 输入框回车或按发送键,prompt 已 trim。 |
keep / discard | () | 结果态的保留 / 放弃。 |
retry | () | 结果态的重试。 |
update:expanded | (expanded: boolean) | 折叠组开合。 |
update:prompt | (prompt: string) | 输入内容变化。 |
Slots
| 插槽名 | 作用域参数 | 说明 |
|---|---|---|
action-icon | { action } | 覆盖动作图标。自定义 id 必须用它。 |
busy | { label } | 覆盖忙碌区。 |
result | — | 覆盖保留 / 放弃 / 重试整组。 |
Exposed
| 方法 | 说明 |
|---|---|
updatePosition() | 按当前 selection.rects 重新定位。流式宿主必须调用,见下。 |
focusInput() | 聚焦自由指令输入框。 |
el | 工具条根元素。传给 useSelectionAnchor 的 ignore。 |
交互契约
updatePosition()是宿主的责任,不是可选优化。 工具条锚在一个虚拟参考点上,floating-ui没有可观察的元素,因此不会跟随文本重排。改写流式写入时每个 delta 都会让段落重排,宿主必须在更新完文本后调一次updatePosition(),否则工具条会停在旧位置。窗口 resize 与滚动由组件自己处理。- 锚点是选区最后一行的底边,水平居中于整段选区。 跨三行的选区,工具条落在读者停下的那一行下面,而不是整块的中点。
- 工具条不翻面。 它给
TxBaseAnchor传了disableFlip:靠近视口底部时不跳到选区上方,因为改写途中换边会被读成另一个控件。shift仍在,横向会被推回视口内。 selection是快照,不是实时选区。 聚焦工具条的输入框会清掉浏览器选区;组件持有的是传进来那一份,所以动作仍有目标。useSelectionAnchor内部用range.cloneRange()也是这个原因。- 工具条根节点吞掉
pointerdown(preventDefault),按按钮不会转移焦点、不会毁掉选区。处理函数会放行input、textarea、contenteditable内部的按下,并且用closest()判定而不是比对事件目标本身:输入框外面套着<form>,一旦指针落在输入框边缘一像素之外,按目标比对就会漏判、输入框随之无法聚焦——点它焦点会跑到<body>,打字也没有任何反应。 - 必须接上
ignore,否则工具条会自己消失。 聚焦输入框会让文档选区塌缩,而useSelectionAnchor在工具条未持有焦点时会把塌缩当成读者清除了选区。请传入实例的el:ignore: () => [barRef.value?.el ?? null]。不要用document.querySelector('.tx-bui-selection-actions')——它返回文档中第一个工具条,页面上只要有两个就会取错。 - 折叠的动作在收起时
tabindex="-1",不出现在 Tab 序列里;发送键在prompt为空时同样如此。 - 整条是
role="group"而不是role="toolbar":里面有文本输入框,方向键要留给光标。 thinking与streaming的区别只有一处——前者的文案带微光,后者是普通文本。- 宽度形变走 WAAPI,CSS 媒体查询管不到它,因此组件在脚本里读
prefers-reduced-motion并跳过补间;状态机照常推进,只是宽度直接落位。 - 空白
prompt不会派发submit。
最佳实践
- 流式改写时把
updatePosition()接在文本更新之后、同一帧里调用,别放setTimeout。 useSelectionAnchor的ignore一定要带上工具条本身,否则用户一点输入框,选区收拢,工具条立刻消失。keep/discard之后调clear(),否则快照会一直留着,工具条不退。- 自定义动作给上
busyLabel,「润色中…」比兜底的「编辑中…」准确得多。 root限定到正文容器,别监听整篇文档——页面上任何一次选中都会弹条。- 只读展示场景可以
hidePrompt,留下预设动作即可。
Source
- Component source:
packages/tuffex/packages/components/src/selection-actions/src/TxSelectionActions.vue。 - Composable:
packages/tuffex/packages/components/src/selection-actions/src/use-selection-anchor.ts。 - Types:
packages/tuffex/packages/components/src/selection-actions/src/types.ts。 - 实测覆盖:
selection-actions.test.ts(23 项)验证按选区显隐、role="group"、锚点取最后一行底边与整段水平范围、disableFlip、折叠组的tabindex与aria-expanded、Explain 也派发事件、prompt门控与 trim、忙碌文案与微光分支、结果态三个动作、pointerdown在条上被吞而在输入框放行、根元素被暴露以便宿主把工具条排除在选区塌缩之外;selection-actions-position.test.ts(2 项)从组件一路断言到floating-ui的update确实被调用;use-selection-anchor.test.ts(9 项)覆盖空选区、最小长度、折叠 range、root限定与零尺寸 rect 过滤。 - 移植自 Beautiful UI(https://www.beautifului.dev),© 2026 Shane Levine,MIT。
查看源码
packages/tuffex/packages/components/src/selection-actions/index.ts