← HTML 表单:从控件到提交与校验 / 文本选择 API:读写光标与选区 待审核 21 / 24
text selection API

文本选择 API:读写光标与选区

支持选择的文本控件(<textarea><input type=text|search|tel|url|password>)暴露了一组专门读写「光标 / 选区」的 IDL 接口:用 selectionStart / selectionEnd / selectionDirection 读出当前选区,用 setSelectionRange() / select() / setRangeText() 改写它。这套 API 是 textarea 富文本工具栏、代码编辑器、实时 masking 的底层基础。本页用一个 live 选区 inspector 把每个属性与方法当场点出来,并对照一个不支持这套 API 的 type=number 控件,看它如何返回 null 与抛错。

1 · selectionStart / selectionEnd / selectionDirection:实时读选区

在下面的 <textarea> 里拖选、移动光标或敲键,右侧 readout 随 select / keyup / mouseup / click 事件刷新。selectionStartselectionEnd 是选区两端的code-unit 偏移(UTF-16);二者相等时即没有选区,该值就是光标位置。selectionDirection 描述用户从哪头拖到哪头——'forward' / 'backward',无选区时为 'none'

偏移按 UTF-16 code unit 计,和 maxlength 同源。selectionStart 等偏移与 value.length 用的是同一把尺子——UTF-16 code unit,不是字符(code point)也不是字素(grapheme)。一个 emoji 或星外字符占2 个 code unit,拖选它时偏移会跳 2。这与 maxlength 截断时的计数完全一致,处理含 emoji 的文本时务必意识到这点。

2 · setRangeText:替换选区的四种 selectMode

setRangeText(replacement[, start, end[, selectMode]])[start, end) 区间(省略则作用于当前选区)替换成 replacement,并用 selectMode 决定替换后选区落在哪里。上面「包 **\dots **」按钮传的是 'select',所以替换出的文本会被整段重新选中,方便链式操作;而默认 'preserve' 会尽量保住替换前用户的选区相对位置。

selectMode 替换后选区落点
'select' 选中整段新插入的文本。适合「插入后想立刻继续操作它」(如加粗工具栏)。
'start' 把光标折叠到新文本的开头(start = end = 插入起点)。
'end' 把光标折叠到新文本的末尾。适合「插入片段后继续往后打字」。
'preserve'(默认) 尽量保持调用前的选区:按新旧文本长度差平移原 start / end,使其仍指向语义上相同的位置。

3 · 对照:type=number 不支持这套 API

选择 API 只对纯文本缓冲的控件有效。下面是一个 type=number 的 input——读它的 selectionStart 会得到 null,调用 setSelectionRange() 会抛 InvalidStateError。点按钮看实际结果(已用 try / catch 捕获异常)。

控件类型 支持选择 API?
<textarea> 支持——多行纯文本缓冲。
input type=text 支持。
input type=search / tel / url 支持——同属单行纯文本控件。
input type=password 支持(值是纯文本,只是显示被遮蔽)。
$input type=number / email / date / color / range \dots$ 不支持——读 selectionStart 返回 null,调 setSelectionRangeInvalidStateError

为什么 number / email / date 不支持?这些控件的 value 是经过解析的结构化值(数字、邮箱、日期),不是一段可任意定位的纯文本缓冲——用户看到的渲染形式与底层 value 未必逐字符对应,「第 3 个 code unit」没有明确含义。规范因此规定它们读 selectionStartnull、调写选区方法抛 InvalidStateError

4 · 一段代码看全

把上面几步串成一段可复制的最小示例——先读出选区,再改写文本,最后把光标定位到工具栏想要的位置:

const ta = document.querySelector('textarea');

ta.selectionStart;
ta.selectionEnd;
ta.selectionDirection;

ta.select();
ta.setSelectionRange(0, 8);
ta.setSelectionRange(0, 8, 'backward');

const s = ta.selectionStart;
const e = ta.selectionEnd;
const inner = ta.value.slice(s, e);
ta.setRangeText('**' + inner + '**', s, e, 'select');

ta.addEventListener('select', updateToolbar);

对照 type=number:同样的两个调用在它身上直接失效。

numberInput.selectionStart;
numberInput.setSelectionRange(0, 3);

这与上文 type=number 对照 demo 的实测结果一致:前者返回 null,后者抛出 InvalidStateError

这套 API 是富文本工具栏与代码编辑器的地基。「选中文字点加粗」「在光标处插入链接 / 表情」「Tab 缩进选中的若干行」这类交互,本质都是读出 selectionStart / selectionEnd、用 setRangeText 改写、再用 setSelectionRange 复位光标。选区变化会派发 select 事件,可据此实时更新工具栏的激活态。