利用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 相关文章推荐
js用图作提交按钮或超连接
Mar 26 Javascript
jQuery maxlength文本字数限制插件
Apr 16 Javascript
javascript中删除指定数组中指定的元素的代码
Feb 12 Javascript
javascript中简单的进制转换代码实例
Oct 26 Javascript
JQuery中ajax方法访问web服务实例
Jul 18 Javascript
jquery实现可关闭的倒计时广告特效代码
Sep 02 Javascript
jQuery焦点图轮播插件KinSlideshow用法分析
Jun 08 Javascript
浅析如何利用angular结合translate为项目实现国际化
Dec 08 Javascript
小程序如何使用分包加载的实现方法
May 22 Javascript
JS实现简单的文字无缝上下滚动功能示例
Jun 22 Javascript
原生js实现下拉选项卡
Nov 27 Javascript
vue实现可移动的悬浮按钮
Mar 04 Vue.js
微信小程序图片选择区域裁剪实现方法
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
谈谈新手如何学习PHP
2006/12/23 PHP
php PDO实现的事务回滚示例
2017/03/23 PHP
PHP实现的文件上传类与用法详解
2017/07/05 PHP
详谈symfony window下的安装 安装时候出现的问题以及解决方法
2017/09/28 PHP
Laravel Intervention/image图片处理扩展包的安装、使用与可能遇到的坑详解
2017/11/14 PHP
php获取是星期几的的一些常用姿势
2019/12/15 PHP
PHP常用字符串输出方法分析(echo,print,printf及sprintf)
2021/03/09 PHP
JS中简单的实现像C#中using功能(有源码下载)
2007/01/09 Javascript
JavaScript快速检测浏览器对CSS3特性的支持情况
2012/09/26 Javascript
关于jQuery对象数据缓存Cache原理以及jQuery.data详解
2013/04/07 Javascript
JavaScript获取多个数组的交集简单实例
2013/11/11 Javascript
js 弹出框只弹一次(二次修改之后的)
2013/11/26 Javascript
使用jQuery实现返回顶部
2015/01/26 Javascript
js实现背景图片感应鼠标变化的方法
2015/02/28 Javascript
jquery仿QQ登录账号选择下拉框效果
2016/03/22 Javascript
每日十条JavaScript经验技巧(二)
2016/06/23 Javascript
在javascript中使用com组件的简单实现方法
2016/08/17 Javascript
ionic实现可滑动的tab选项卡切换效果
2020/04/15 Javascript
js实现选项卡内容切换以及折叠和展开效果【推荐】
2017/01/08 Javascript
JS正则获取HTML元素的方法
2017/03/31 Javascript
JScript实现表格的简单操作
2017/08/15 Javascript
手机注册发送验证码倒计时的简单实例
2017/11/15 Javascript
vue中axios解决跨域问题和拦截器的使用方法
2018/03/07 Javascript
vue项目实现github在线预览功能
2018/06/20 Javascript
[46:42]DOTA2-DPC中国联赛正赛 Aster vs Magma BO3 第二场 3月5日
2021/03/11 DOTA
星球大战与Python之间的那些事
2016/01/07 Python
Windows环境下python环境安装使用图文教程
2018/03/13 Python
解决python os.mkdir创建目录失败的问题
2018/10/16 Python
three.js模拟实现太阳系行星体系功能
2019/09/03 HTML / CSS
html5简介及新增功能介绍
2020/05/18 HTML / CSS
北美Newegg打造的全球尖货海购平台:tt海购
2018/09/28 全球购物
都柏林通行卡/城市通票:The Dublin Pass
2020/02/16 全球购物
介绍java中初始化块的使用
2012/09/11 面试题
挂职思想汇报
2013/12/31 职场文书
财经学院自荐信范文
2014/02/02 职场文书
创业分两种人:那么哪些适合创业?,哪些适合不适合创业呢?
2019/08/23 职场文书