本文介绍如何利用浏览器原生 Selection API 实现流畅的双向文本高亮功能——当用户按住鼠标拖拽时,无论正向还是反向移动光标,高亮区域均能实时跟随并自动修正,彻底解决传统 mousemove 方案中方向切换导致的高亮断裂问题。
本文介绍如何利用浏览器原生 selection api 实现流畅的双向文本高亮功能——当用户按住鼠标拖拽时,无论正向还是反向移动光标,高亮区域均能实时跟随并自动修正,彻底解决传统 `mousemove` 方案中方向切换导致的高亮断裂问题。
在实现交互式文本标注(如教育工具、法律文档批注或内容分析系统)时,一个常见痛点是:基于 mousedown + mousemove 的手动范围计算逻辑难以准确响应鼠标方向突变——例如用户从左向右拖选后突然折返向左拖动,旧方案往往残留错误高亮或无法收缩已选区域。
根本原因在于,mousemove 事件仅反映光标瞬时位置,缺乏对用户意图选择范围的语义理解;而浏览器原生的 Selection 和 Range API 天然具备方向无关的范围抽象能力。它自动处理光标反向拖拽、跨行选择、DOM 结构变更等复杂场景,是我们重构高亮逻辑的理想基础。
✅ 核心思路:用 Selection 替代手动坐标计算
我们不再监听 mousemove 并维护 startIndex/endIndex,而是:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- mousedown 时重置样式规则:清除旧的 ::selection 高亮色,注入新颜色;
- mouseup 时提取真实选区:调用 window.getSelection() 获取用户最终选定的 DOM 节点;
- 批量应用背景色:遍历选中节点,通过 data-index 定位对应 ,设置 style.backgroundColor;
- 统一管理历史记录:用数组 previousHiglight 存储每次操作的索引列表,便于 Undo 精确回滚。
以下是关键代码实现(已精简整合,可直接嵌入项目):
// 【1】全局状态管理
const previousHiglight = []; // 存储每次高亮的 word index 数组
let currentColor = '';
const availableColors = ["yellow", "red", "blue", "green", "orange"];
const usedColors = new Set();
function getRandomColor() {
if (availableColors.length === 0) {
availableColors.push(...usedColors);
usedColors.clear();
}
const idx = Math.floor(Math.random() * availableColors.length);
const color = availableColors.splice(idx, 1)[0];
usedColors.add(color);
return color;
}
// 【2】mousedown:动态更新 ::selection 样式
document.getElementById('content').addEventListener('mousedown', (e) => {
// 清除现有 ::selection 规则
const sheet = document.styleSheets[0];
for (let i = sheet.cssRules.length - 1; i >= 0; i--) {
if (sheet.cssRules[i].selectorText?.includes('::selection')) {
sheet.deleteRule(i);
}
}
// 注入新高亮色
currentColor = getRandomColor();
sheet.insertRule(`#content span::selection { background-color: ${currentColor}; }`, sheet.cssRules.length);
});
// 【3】mouseup:解析 Selection 并高亮真实节点
document.getElementById('content').addEventListener('mouseup', (e) => {
const sel = window.getSelection();
if (!sel.rangeCount || sel.toString().trim() === '') return;
const indexes = [];
const contentDiv = document.getElementById('content');
// 遍历所有 Range(兼容多段选择)
for (let i = 0; i {
const idx = span.dataset.index;
if (idx && !isHighlighted(idx)) {
contentDiv.querySelector(`[data-index="${idx}"]`).style.backgroundColor = currentColor;
indexes.push(idx);
}
});
}
if (indexes.length > 0) {
previousHiglight.push(indexes);
}
sel.removeAllRanges(); // 清除原生选区,避免视觉干扰
});
// 【4】辅助函数:判断是否已高亮
function isHighlighted(index) {
for (const indexes of previousHiglight) {
if (indexes.includes(index)) return true;
}
return false;
}
// 【5】Undo:回滚最后一次高亮
document.getElementById('undoHighlight').addEventListener('click', () => {
if (previousHiglight.length === 0) return;
const lastIndexes = previousHiglight.pop();
const contentDiv = document.getElementById('content');
lastIndexes.forEach(idx => {
const el = contentDiv.querySelector(`[data-index="${idx}"]`);
if (el) el.style.backgroundColor = '';
});
});
// 【6】Clear:清空全部高亮
document.getElementById('removeHighlight').addEventListener('click', () => {
const contentDiv = document.getElementById('content');
contentDiv.querySelectorAll('span[data-index]').forEach(el => {
el.style.backgroundColor = '';
});
previousHiglight.length = 0;
});
⚠️ 注意事项与最佳实践
- ::selection 兼容性:现代浏览器均支持,但 Safari 对 ::selection 在非编辑元素中生效需确保父容器可聚焦(建议为 #content 添加 tabindex="0");
- 性能优化:若文本量极大(>10k 单词),querySelectorAll 可替换为 getElementById 或缓存 span 引用数组;
- 语义准确性:本方案以 为最小高亮单元,天然规避了单词内部分高亮问题;如需字符级控制,需改用 Range 拆分文本节点;
- 无障碍支持:高亮色需满足 WCAG 对比度要求(如黄色背景配深灰文字),建议提供色盲友好配色方案;
- 撤销栈扩展:previousHiglight 可升级为包含时间戳、颜色、操作类型(add/remove)的完整日志,支撑多级 Undo/Redo。
通过将高亮逻辑从“手动模拟选择”转向“响应真实 Selection”,我们不仅解决了方向切换难题,更获得了跨浏览器一致性、更低维护成本和更强扩展性——这才是专业文本交互体验的正确起点。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










