详解javascript中的Selection和Range对象

写在前面:`JavaScript`中的`Selection`和`Range`对象,对于没有开发过文本编辑器或者没有接触过文本选择项目的同学,熟悉而陌生,熟悉是知道他们大致是干嘛的,陌生就是从来没用过,具体怎么用还得查手册。本文以有道词典`chrome`翻译插件的实现为例,来详细讲解一下这两个对象的具体使用

至于为什么会用到有道词典chrome翻译插件[下文中都简称有道插件]?那当然是为了方便查阅[www.w3.org](https://www.w3.org/)上面的文档啦...,当老板过来找你的时候发现你在看英文文档,你懂的😀...

有的同学可能不知道有道插件怎么用?[点这里](https://chrome.google.com/webstore/detail/%E6%9C%89%E9%81%93%E8%AF%8D%E5%85%B8chrome%E5%88%92%E8%AF%8D%E6%8F%92%E4%BB%B6/eopjamdnofihpioajgfdikhhbobonhbb?hl=zh-CN)。自带梯子,GFW,这个大家也都懂的...

chrome浏览器安装好有道翻译插件的效果图如下

![](selection-img/chrome-yd-readme-show.png)

上图中大家会看到有道插件的工作方式有2种,本文着重介绍第二种方式的实现思路,也就是按住`Ctrl`键自动拾取英文单词进行翻译

  1. 划词翻译

  2. 指此即译模式(按下Ctrl键指词)

再上一张有道插件的翻译效果图:

![chrome-ex-yd-1.png](selection-img/chrome-ex-yd-1.png)

正式开始前,我们先了解下有道插件的工作原理和步骤

  1. 获取到目标单词

  2. 将单词文本通过`ajax`请求发送给有道服务器获取翻译结果

  3. 将翻译结果展示到页面上

这么一来,划词翻译就很好理解了。按住鼠标左键拖动选中目标单词,发起ajax请求服务器获得翻译结果,将翻译结果展示到页面上

大家可能会问了,鼠标拖动选中的部分是怎么被`javascript`获取的?这就得聊聊我们今天的两位主角`Selection`对象和`Range`对象了

##正文

简单来说:按住鼠标左键拖动选择目标区域后,浏览器会自动创建一个选区,也就是`Selection`对象,这个选区里连续的文本区域就是一个`Range`对象,直接上代码(别问代码是谁)

`var selection = document.getSelection();

var range = selection.getRangeAt(0);`

那么这两个对象是有了,可是跟我们要获取选中的文本貌似没有太大关系?还是上代码

`var str = range.toString();

console.log(str);`

就是这样简单,我们只需要三行js代码就可以获取到用户选中的目标文本,so easy...

OK,大家可以抬头看黑板了,现在开始说重点:有道插件是如何做到按住ctrl键来自动拾取页面中的英文单词的?这里,我么会拆成几个步骤一步一步来实现,go on

### 第一步:监听鼠标移动事件,获取鼠标当前悬停的坐标

还是直接看代码

`document.onmousemove = function(event){

if (!event.ctrlKey){

return true;

}

var pX = event.pageX;

var pY = event.pageY;

}`

监听`mousemove`事件,可以通过`event.ctrlKey`来判断ctrl键是否被按下了,`event.clientX和event.clientY`可以获取当前鼠标对应的坐标,那到目前为止,我们的准备工作就已经完成了

### 第二步:获取鼠标所在坐标的英文单词

回想一下有道插件翻译的效果图,当你按住ctrl键,鼠标移动到某个单词上方的时候,这个单词底部变成了选中的蓝色,就像是我们自己拖动鼠标选中了一样?

没错,实现逻辑就是:当鼠标移动到任意位置的时候,获取到当前鼠标坐标,然后`查找`该坐标附近的唯一英文单词,找到该单词后,创建选区(Selection)以及拖蓝(Range),就出现了我们看到的单词被选中的效果,然后再通过Range.toString()获取目标单词

这里有个新概念-拖蓝:Selection对象所对应的是用户所选择的 ranges (区域),俗称拖蓝

另外,细心的同学会问,到底是如何`查找`的呢?接下来就是... 我们先介绍几个基本概念,欲速则不达,我们要先弄明白一些基本问题,比如说

#### 我们如何根据一个坐标来创建一块拖蓝?有哪些方法可以创建拖蓝?

创建拖蓝(也就是Range对象),有以下4种方法

1.`var range = document.createRange()`通过document对象的createRange()方法创建空的Range对象

  1. `var selection = document.getSelection();

var range = selection.getRangeAt(0)`

通过Selection对象的getRangeAt()方法获取Range对象

  1. `var range = document.caretRangeAtPoint()`

通过document对象的caretRangeAtPoint(x,y)来创建Range对象

  1. `var range = new Range()`通过构造函数创建Range对象

我们通过几个示例来讲这几种方法的不同

方法1,`document.createRange()`,直接上代码

`<p id="p1"><b>Hello</b> World</p>

<script>

var oP1 = document.getElementById("p1");

var oHello = oP1.firstChild.firstChild;

//获取文本节点Hello

var oWorld = oP1.lastChild;

//获取文本节点 World,注意这里以空格开始

var oRange = document.createRange();

//创建空的Range对象

oRange.setStart(oHello, 2);

//设置Range对象的起始节点为oHello

//设置Range对象的文本起始位置为节点oHello的第2个位置

oRange.setEnd(oWorld, 3);

//设置Range对象的结束节点为oWorld

//设置Range对象的文本结束位置为节点oWorld的第3个位置

var s = window.getSelection();

//创建空Selection对象s

s.removeAllRanges();

//删除当前Selection对象里其他Range对象

s.addRange(oRange);

//将oRange对象添加到Selection对象s

</script>`

效果图如下

![](selection-img/ex-hello-world.png)

这里有几个重要概念`Range.setStart(node, offset)`,`Range.setEnd(node, offset)`。`Range`对象的这两个函数分别可以调整`Range`对象文本的起始位置和结束位置。

创建好了`Range`对象并设置了起始位置和结束位置之后,你会发现浏览器里面并不会出现选中效果,也就是我们所说的拖蓝区域,那是因为该`Range`对象只存在于我们的脚本里,我们并没有让它在浏览器页面里`长`出来,那怎么让这个`Range`对象成长为拖蓝呢?就是下面三行代码干的好事

`var s = window.getSelection();

s.removeAllRanges();

s.addRange(oRange);`

我们要创建选区`Selection`对象`s`,并把创建的`Range`对象`oRange`添加到`Selection`对象`s`,大工搞成,拖蓝长出来了。

细心的朋友又会问了,那`Selection`对象和`Range`对象到底是什么关系呢?

简单来说:`Selection`对象称为选区,它可以包含多个`Range`对象,也就是同一时刻,浏览器只能有一个选区(`Selection`),而该选区可以包含多个拖蓝区域(`Range`)

经测试,`firefox`按住`ctrl`键的时候,可以手动选择多个拖蓝[脚本相同],`chrome`目前是禁用了`ctrl`选择多个拖蓝的功能,IE 也不支持,如下是`firefox`的效果截图

![chrome-ex-yd-multi-range.png](selection-img/chrome-ex-yd-multi-range.png)

如果尝试用javascript脚本在chrome下创建多个range则会报错如下截图[直译就是:不连续的selection是不被支持的]:

![chrome-no-multi-range](selection-img/chrome-no-multi-range.png)

方法2:`var selection = document.getSelection();

var range = selection.getRangeAt(0)`

`document.getSelection()`获取到选区对象`selection`,并使用`Selection`对象的`getRangeAt()`方法获取`Range`对象。这个方法也同时说明了`Selection`对象和`Range`对象的关系

方法3:没错,它就是我们的心仪对象,通过坐标创建Range对象 `var r = document.caretRangeFromPoint(event.clientX, event.clientY);`,那这个`Range`对象如何和它周围的文本节点关联呢?答案就是下面的4个属性

* `Range.startContainer`:`Range`对象起始位置所在节点

* `Range.startContainer.data`:`Range`对象起始位置所在节点的文本字符串

* `Range.endContainer`:`Range`对象结束位置所在节点

* `Range.endContainer.data`:`Range`对象结束位置所在节点的文本字符串

通过坐标创建的`Range`对象起始位置和结束位置是相同的,我们的逻辑就是循环往前移动起始位置,直到遇到非英文字符停止;循环往后移动结束位置,直到遇到非英文字符停止(通过Range.setStart()和Range.setEnd()来移动起始/结束位置),最终我们创建的`Range`对象就会拾取到目标单词

方法4构造函数创建`Range`对象类似方法1 `var range = new Range();`

#### 如何创建Selection对象,有哪几种方法?

获取Selection对象有2种方法,其作用是等同的

* `var selection = window.getSelection()`

* `var selection = document.getSelection()`

OK,讲完了基本知识,迎来我们的大结局,直接上代码[实现简易单词拾取效果,代码略长哟]

`<!doctype html>

<html>

<head>

<title>有道词典chrome插件拾取单词核心代码</title>

<script>

var preWord = "";

var isAlpha = function(str){return /[a-zA-Z']+/.test(str)};

//判断当前字符串是否符为英文字符串

document.onmousemouve = function(event){

if (!event.ctrlKey){

return true;

}

//如果control键没有被按下则直接返回,不做操作

var r = document.caretRangeFromPoint(event.clientX, event.clientY);

//以当前鼠标所在位置创建空Range对象

if (!r) return true;

var pX = event.pageX;

var pY = event.pageY;

var so = r.startOffset;

//获取当前Range对象的起始位置

var eo = r.endOffset;

//获取当前Range对象的结束位置

var tr = r.cloneRange();

//克隆当前Range对象赋值给变量tr

var text='';

//循环判断并确定Range对象的起始位置

if (r.startContainer.data) while (so >= 1){

//Range.startContainer返回Range对象起始点所在的节点

//Range.startContainer.data则返回Range对象起始点所在节点的文本内容

tr.setStart(r.startContainer, --so);

//往前查找,移动Range对象的起始点位置,直到遇到非英文字符

text = tr.toString();

//获取当前Range对象的字符串表示

if (!isAlpha(text.charAt(0))){

tr.setStart(r.startContainer, so + 1);

//遇到非英文字符之后,将Range对象的起始点移动到后一位正确位置

break;

}

}

//同上,循环判断并确定Range对象的结束位置

if (r.endContainer.data) while (eo < r.endContainer.data.length){

tr.setEnd(r.endContainer, ++eo);

text = tr.toString();

if (!isAlpha(text.charAt(text.length - 1))){

tr.setEnd(r.endContainer, eo - 1);

break;

}

}

var word = tr.toString();

//获取当前拾取到的英文单词

if(!word.length)return;

var s = window.getSelection();

//创建空Selection对象

s.removeAllRanges();

//删除其他所有Range对象

s.addRange(tr);

//将当前Range对象添加到Selection对象

if(preWord == word)return;

//简单判断当前单词是否已经被选中

preWord = word;

var showDiv = document.getElementById("selectionP");

if(showDiv)

{

document.body.removeChild(showDiv);

}

//创建绝对定位的div,将选中单词作为内容展示到当前页面

var p = document.createElement("div");

p.innerText = word;

p.style.position = "absolute";

p.style.backgroundColor = '#ccc';

p.style.width = "100px";

p.style.height = "50px";

p.id = "selectionP";

p.style.left = e.pageX+"px";

p.style.top = e.pageY+"px";

document.body.appendChild(p);

}

</script>

</head>

<body>

<p class="">文本1 content The Editing Task Force can be followed on our mailing list. We track issues in GitHub Issues as well.

See the Task Force's charter for more information about the group.<span>hello, i am last</span></p>

</body>

</html>`

效果图如下

![](selection-img/simple-yd-1.png)

##后记

* 测试代码的时候,有一个坑就是`mousemove`或者`mouseup`事件执行如上代码都是OK的,但是`mousedown`事件则不会产生拖蓝(选中效果)。这个是因为鼠标按下事件发生时,通过脚本创建了拖蓝,但是会紧接着执行`mouseup`事件,这个时候,浏览器会自动创建新的选区,从而清除掉我们已经创建好的拖蓝区域

*这里讲的`api`都是标准`api`,IE8-等浏览器有自己特殊的类似`api`

* 有道插件展示翻译结果的时候,涉及到元素定位,这个可以单独开一篇文章来讲了,这里不再赘述

*`chrome`插件开发其实很简单,就是根据`chrome`官方规则,把我们的`html,js,css`文件打包成`.crx`文件后,上传到`chrome`商店,就变成了插件,所以作为前端同学,开发`chrome`插件是很容易的,[如何开发一个chrome插件](https://developer.chrome.com/extensions)

*`chrome`下如下两行代码的执行结果是一致的,这个也很好理解,因为`chrome`限制了一个`Selection`对象只能对应一个`Range`对象

* `console.log(Selection.toString());`

* `console.log(Range.toString());`

(完)

results for ""

    No results matching ""