利用Javascript获取选择文本所在的句子详解


Posted in Javascript onDecember 03, 2017

前言

最近收到一个 issue 期望能在划词的时候同时保存单词的上下文和来源网址。这个功能其实很久之前就想过,但感觉不好实现一直拖延没做。真做完发现其实并不复杂,完整代码在这里,或者继续往下阅读分析。话不多说了,来一起看看详细的介绍吧。

原理分析

获取选择文本

通过 window.getSelection() 即可获得一个 Selection 对象,再利用 .toString() 即可获得选择的文本。

锚节点与焦节点

在 Selection 对象中还保存了两个重要信息,anchorNode 和 focusNode,分别代表选择产生那一刻的节点和选择结束时的节点,而 anchorOffset 和 focusOffset 则保存了选择在这两个节点里的偏移值。

这时你可能马上就想到第一个方案:这不就好办了么,有了首尾节点和偏移,就可以获取句子的头部和尾部,再把选择文本作为中间,整个句子不就出来了么。

当然不会这么简单哈stuck_out_tongue。

强调一下

一般情况下,anchorNode 和 focusNode 都是 Text 节点(而且因为这里处理的是文本,所以其它情况也会直接忽略),可以考虑这种情况:

<strong>Saladict</strong> is awesome!

如果选择的是“awesome”,那么 anchorNode 和 focusNode 都是 is awesome!,所以取不到前面的 “Saladict”。

另外还有嵌套的情况,也是同样的问题。

Saladict is <strong><a href="#" rel="external nofollow" >awesome</a></strong>!

所以我们还需要遍历兄弟和父节点来获取完整的句子。

遍历到哪?

于是接下就是解决遍历边界的问题了。遍历到什么地方为止呢?我的判断标准是:跳过 inline-level 元素,遇到 block-level 元素为止。而判断一个元素是 inline-level 还是 block-level 最准确的方式应该是用 window.getComputedStyle() 。但我认为这么做太重了,也不需要严格的准确性,所以用了常见的 inline 标签来判断。

const INLINE_TAGS = new Set([
 // Inline text semantics
 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em', 'i',
 'kbd', 'mark', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'small',
 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr'
])

原理总结

句子由三块组成,选择文本作为中间,然后遍历兄弟和父节点获取首尾补上。

实现

选择文本

先获取文本,如果没有则退出

const selection = window.getSelection()
const selectedText = selection.toString()
if (!selectedText.trim()) { return '' }

获取首部

对于 anchorNode 只考虑 Text 节点,通过 anchorOffset 获取选择在 anchorNode 的前半段内容。

然后开始补全在 anchorNode 之前的兄弟节点,最后补全在 anchorNode 父元素之前的兄弟元素。注意后面是元素,这样可以减少遍历的次数,而且考虑到一些被隐藏的内容不需要获取,用 innerText 而不是 textContent 属性。

let sentenceHead = ''
const anchorNode = selection.anchorNode
if (anchorNode.nodeType === Node.TEXT_NODE) {
 let leadingText = anchorNode.textContent.slice(0, selection.anchorOffset)
 for (let node = anchorNode.previousSibling; node; node = node.previousSibling) {
 if (node.nodeType === Node.TEXT_NODE) {
 leadingText = node.textContent + leadingText
 } else if (node.nodeType === Node.ELEMENT_NODE) {
 leadingText = node.innerText + leadingText
 }
 }

 for (
 let element = anchorNode.parentElement;
 element && INLINE_TAGS.has(element.tagName.toLowerCase()) && element !== document.body;
 element = element.parentElement
 ) {
 for (let el = element.previousElementSibling; el; el = el.previousElementSibling) {
 leadingText = el.innerText + leadingText
 }
 }

 sentenceHead = (leadingText.match(sentenceHeadTester) || [''])[0]
}

最后从提取句子首部用的正则是这个

// match head   a.b is ok chars that ends a sentence
const sentenceHeadTester = /((\.(?![ .]))|[^.?!。?!…\r\n])+$/

前面的 ((\.(?![ .])) 主要是为了跳过 a.b 这样的特别是在技术文章中常见的写法。

获取尾部

跟首部同理,换成往后遍历。最后的正则保留了标点符号

// match tail       for "..."
const sentenceTailTester = /^((\.(?![ .]))|[^.?!。?!…\r\n])+(.)\3{0,2}/

压缩换行

拼凑完句子之后压缩多个换行为一个空白行,以及删除每行开头结尾的空白符

return (sentenceHead + selectedText + sentenceTail)
 .replace(/(^\s+)|(\s+$)/gm, '\n') // allow one empty line & trim each line
 .replace(/(^\s+)|(\s+$)/g, '') // remove heading or tailing \n

完整代码

const INLINE_TAGS = new Set([
 // Inline text semantics
 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em', 'i',
 'kbd', 'mark', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'small',
 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr'
])

/**
* @returns {string}
*/
export function getSelectionSentence () {
 const selection = window.getSelection()
 const selectedText = selection.toString()
 if (!selectedText.trim()) { return '' }

 var sentenceHead = ''
 var sentenceTail = ''

 const anchorNode = selection.anchorNode
 if (anchorNode.nodeType === Node.TEXT_NODE) {
 let leadingText = anchorNode.textContent.slice(0, selection.anchorOffset)
 for (let node = anchorNode.previousSibling; node; node = node.previousSibling) {
 if (node.nodeType === Node.TEXT_NODE) {
 leadingText = node.textContent + leadingText
 } else if (node.nodeType === Node.ELEMENT_NODE) {
 leadingText = node.innerText + leadingText
 }
 }

 for (
 let element = anchorNode.parentElement;
 element && INLINE_TAGS.has(element.tagName.toLowerCase()) && element !== document.body;
 element = element.parentElement
 ) {
 for (let el = element.previousElementSibling; el; el = el.previousElementSibling) {
 leadingText = el.innerText + leadingText
 }
 }

 sentenceHead = (leadingText.match(sentenceHeadTester) || [''])[0]
 }

 const focusNode = selection.focusNode
 if (selection.focusNode.nodeType === Node.TEXT_NODE) {
 let tailingText = selection.focusNode.textContent.slice(selection.focusOffset)
 for (let node = focusNode.nextSibling; node; node = node.nextSibling) {
 if (node.nodeType === Node.TEXT_NODE) {
 tailingText += node.textContent
 } else if (node.nodeType === Node.ELEMENT_NODE) {
 tailingText += node.innerText
 }
 }

 for (
 let element = focusNode.parentElement;
 element && INLINE_TAGS.has(element.tagName.toLowerCase()) && element !== document.body;
 element = element.parentElement
 ) {
 for (let el = element.nextElementSibling; el; el = el.nextElementSibling) {
 tailingText += el.innerText
 }
 }

 sentenceTail = (tailingText.match(sentenceTailTester) || [''])[0]
 }

 return (sentenceHead + selectedText + sentenceTail)
 .replace(/(^\s+)|(\s+$)/gm, '\n') // allow one empty line & trim each line
 .replace(/(^\s+)|(\s+$)/g, '') // remove heading or tailing \n
}

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
javascript解决innerText浏览器兼容问题思路代码
May 17 Javascript
运用jQuery定时器的原理实现banner图片切换
Oct 22 Javascript
谈谈PHP中相对路径的问题与绝对路径的使用
Aug 16 Javascript
浅谈Javascript中的12种DOM节点类型
Aug 19 Javascript
原生JS实现导航下拉菜单效果
Nov 25 Javascript
vue中用H5实现文件上传的方法实例代码
May 27 Javascript
详解Js中的模块化是如何实现的
Oct 18 Javascript
详解Angular结合zTree异步加载节点数据
Jan 20 Javascript
JavaScript生成指定范围随机数和随机序列的方法
May 05 Javascript
利用js将ajax获取到的后台数据动态加载至网页中的方法
Aug 08 Javascript
基于vue2的canvas时钟倒计时组件步骤解析
Nov 05 Javascript
JavaScript事件循环及宏任务微任务原理解析
Sep 02 Javascript
微信小程序图片选择区域裁剪实现方法
Dec 02 #Javascript
vue中eventbus被多次触发以及踩过的坑
Dec 02 #Javascript
Angular之toDoList的实现代码示例
Dec 02 #Javascript
React Native 使用Fetch发送网络请求的示例代码
Dec 02 #Javascript
vue微信分享 vue实现当前页面分享其他页面
Dec 02 #Javascript
Vue按需加载的具体实现
Dec 02 #Javascript
使用Vue完成一个简单的todolist的方法
Dec 01 #Javascript
You might like
BBS(php &amp; mysql)完整版(八)
2006/10/09 PHP
利用discuz自带通行证整合dedecms的方法以及文件下载
2007/03/06 PHP
使用php实现下载生成某链接快捷方式的解决方法
2013/05/07 PHP
Ajax实现对静态页面的文章访问统计功能示例
2016/10/10 PHP
WordPress伪静态规则设置代码实例
2020/12/10 PHP
网页里控制图片大小的相关代码
2006/06/13 Javascript
js实现的网页颜色代码表全集
2007/07/17 Javascript
javascript iframe内的函数调用实现方法
2009/07/19 Javascript
JavaScript 输入框内容格式验证代码
2010/02/11 Javascript
获取焦点时,利用js定时器设定时间执行动作
2010/04/02 Javascript
动态加载脚本提升javascript性能
2014/02/24 Javascript
JS实现鼠标单击与双击事件共存
2014/03/08 Javascript
JS中script标签defer和async属性的区别详解
2016/08/12 Javascript
微信小程序ibeacon三点定位详解
2018/10/31 Javascript
小试SVG之新手小白入门教程
2019/01/08 Javascript
基于vue-cli 路由 实现类似tab切换效果(vue 2.0)
2019/05/08 Javascript
VUE中鼠标滚轮使div左右滚动的方法详解
2020/12/14 Vue.js
[49:05]Newbee vs TNC 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
Python使用PIL模块生成随机验证码
2017/11/21 Python
django使用xlwt导出excel文件实例代码
2018/02/06 Python
python实现彩色图转换成灰度图
2019/01/15 Python
Python中的字符串切片(截取字符串)的详解
2019/05/15 Python
Django中使用极验Geetest滑动验证码过程解析
2019/07/31 Python
如何在django中添加日志功能
2020/02/06 Python
keras和tensorflow使用fit_generator 批次训练操作
2020/07/03 Python
Python图像处理二值化方法实例汇总
2020/07/24 Python
化工专业个人的求职信范文
2013/11/28 职场文书
档案管理员岗位职责
2013/12/01 职场文书
英文简历中的自荐信范文
2013/12/14 职场文书
五一家具促销方案
2014/01/10 职场文书
市场营销管理制度
2014/01/29 职场文书
土木工程专业本科生求职信
2014/10/01 职场文书
会计简历自我评价
2015/03/10 职场文书
民事辩护词范文
2015/05/21 职场文书
大学生暑期实践报告之企业经营管理
2019/08/08 职场文书
2019年“我为祖国点赞”演讲稿(3篇)
2019/09/26 职场文书