目录
- 场景介绍
- selection介绍
- selection API
- range 介绍
- range API
- 实现编辑区插入表情图片
- 参考资料
场景介绍
在写web
版聊天器时,遇到一个需求:
聊天时用户可以在编辑区加入表情图片,并且表情图片要插入在光标位置。
// *web版聊天器地址:
http://81.68.151.74/ (开发中,欢迎讨论)
就像这样:
刚开始没考虑这块内容,所以用的<textarea>
标签,
而在该标签内,是无法显示
出插入表情图片的。
这就需要用到HTML
中的contenteditable="true"
属性,来创建一个自定义编辑区。
就像这样:
<div class="box" contenteditable="true"></div>
然后对该自定义编辑区进行加工。
此过程中,需要根据用户点击的位置(即光标显示的位置)来显示插入的图片,
这就需要用到光标的控制。
selection介绍
光标控制的API
分为两块内容:selection
选区和range
片段。
根据官方介绍,selection
对象所对应的是用户所选择的 ranges
(区域),俗称拖蓝
。
默认情况下,该函数只针对一个区域。
因为一般情况下,用拖动鼠标只能同时选中一块区域。
在使用js
脚本的情况下,range
可能会出现多个。
因此,后者可以认为是前者的一部分,选区包含若干片段,一般是一个。
本篇就梳理下两者的常用API
,理清操作流程。
selection API
selection
对象表示用户选择的文本范围或插入符号的当前位置。
它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。
获取selection
:
var selection = window.getSelection();
下面依次介绍selection
的相关属性和方法。
首先需要注意的是,以下内容提到的起点
和终点
,并不是固定的。
这和鼠标从左往右
选择还是从右往左
选择有关系。
先从哪里开始,哪里就是起点。
<!--属性-->
1. selection.anchorNode
只读属性,用于返回选区开始位置所属的节点。
2. selection.anchorOffset
只读属性,返回选区的锚节点( Selection.anchorNode)起点偏移量的数字。
返回值从零开始计数,如果选区从锚节点(Selection.anchorNode)的第一个字符开始,
返回值为 0。
3. selection.focusNode
只读属性,返回所选内容的结束位置部分所属的节点。
4. selection.focusOffset
返回选区终点(鼠标松开瞬间所记录的那个点)在焦点(Selection.focusNode)中的偏移量。
返回值从零开始计数,如果选区(Selection)在焦点(Selection.focusNode)的第一个字符前结束,
返回值为 0。
5. selection.isCollapsed
返回一个布尔值,用于描述选区的起始点和终止点是否位于一个位置,即是否框选了。
6. selection.rangeCount
只读属性。用于返回选区 (selection) 中 range 对象数量
一般rangeCount总是为1,因为拖动鼠标只能选择一个区域。
7. selection.type
只读属性,用于描述当前选区的选择类型。
结果有三个值:
- `None` 当前没有选择。
- `Caret`: 选区已折叠(即 光标在字符之间,并未处于选中状态)。
- `Range`: 选择的是一个范围。即框选了内容。
<!--方法-->
1. selection.addRange(range)
向选区(Selection)中添加一个区域(Range)
2. selection.collapse(node, offset)
设置光标在选区内的位置。
node是光标所落在的目标节点
offset是落在节点的偏移量
3. selection.collapseToEnd()
取消当前选区,并把光标定位在原选区的最末尾处。无参数
4. selection.collapseToStart()
取消当前选区,并把光标定位在原选区的最开始处。无参数
5. selection.containsNode(node,isPartlyContained)
node是要被判断的节点
isPartlyContained表示node节点是否被包含
isPartlyContained为true时,就是要判断选区是否包含node,(此时包含一部分或者全部,都算被包含)
isPartlyContained为false时,就是要判断选区是否包含node的全部
6. selection.deleteFromDocument()
删除选区
7. selection.extend(node, offset)
移动选中区的焦点到指定的点。选中区的锚点不会移动。选中区将从锚点开始到新的焦点,不管方向。
node是表示焦点会被移动到node节点内
offset 可选参数,默认为0.表示偏移量
8. var range = selection.getRangeAt(index)
返回一个包含当前选区内容的区域对象
index表示区域编号,从0开始。index永远要小于rangeCount
rangeCount一般为1,所以index一般是0
9. selection.modify(alter, direction, granularity)
通过简单的文本命令来改变当前选区或光标位置。
alter 改变类型。传入 "move" 来移动光标位置,或者 "extend" 来扩展当前选区。
direction 调整选区的方向。forward backward left right
granularity 调整的距离颗粒度。一次是移动一个字的举例,还是一个字符的距离等。
10. selection.removeAllRanges()
从当前 selection 对象中移除所有的 range 对象,
取消所有的选择只 留下anchorNode 和focusNode属性并将其设置为 null
无参数。
11. selection.removeRange(range)
将一个区域从选区中移除。
range 表示要被移除的区域
12. selection.selectAllChildren(parentNode)
所有 parentNode 元素的子元素会被设为选中区域,
并取消之前的选中区域,parentNode 本身除外。
13. selection.setBaseAndExtent(anchorNode,anchorOffset,focusNode,focusOffset)
用来选中并设置在两个特定的 DOM 节点中文本选中的范围,
并且选中的任何内容都位于两个节点之间。
anchorNode 锚节点 - 选中内容的开始节点
anchorOffset 选中范围内起点位置在锚节点下第几个子节点的位置。
例如,如果是值为 0 的话,整个节点都是被选中的。
如果值为 1 的话,那么至少整个节点至少有一个子节点被选中。以此类推。
focusNode 焦点节点 - 选中内容的结尾节点
focusOffset 选中范围内结束位置在焦点节点下第几个子节点的位置。
例如,如果是值为 0 的话,整个节点都是被选中的。
如果值为 1 的话,那么至少整个节点至少有一个子节点被选中。以此类推
range 介绍
range
表示一部分文档片段。
可以通过多种方法创建或获取:
// 第一种 从selection中获取
var range = selection.getRangeAt();
// 第二种 通过Range创建
var range = new Range()
// 第三种 create创建
var range = document.createRange()
// 注意:
// 如果是创建的range,在使用他的大多数方法之前需要去设置他的临界点。
// 也就是要确定光标
range API
<!--属性-->
1. range.collapsed
返回一个布尔值。表示是否起始点和结束点是同一个位置
如果返回true,就表示起始点和结束点重合。
如果返回false,就表示起始点和结束点没重合。
2. range.endContainer
它会返回Range对象结束的Node
3. range.endOffset
返回代表 Range 结束位置在 Range.endContainer 中的偏移值的数字
4. range.startContainer
返回 Range 开始的节点
5. range.startOffset
用于返回一个表示 Range 在 startContainer 中的起始位置的数字
<!--方法-->
1. range.cloneContents()
克隆一个区域的所有节点。
节点的id属性也会被克隆,这点需要注意。
另外,节点的监听事件不会被克隆
2. range.cloneRange()
克隆一个和原有range无关的range对象
3. range.collapse(toStart)
用于折叠边界点。也就是 是否让光标重合
toStart为true,表示让光标重合,并在range的起始点出现
toStart为false,表示让光标重合,并在range的结束点出现
4. range.insertNode(newNode);
在Range的起始位置插入节点
newNode是要插入的节点
5. range.intersectsNode(node)
用来判断给定的节点node是否与range相交,返回一个布尔值
6. range.selectNode(node);
将 Range 设置为包含整个 node 及其内容。
Range 的起始和结束节点的父节点与 node 的父节点相同。
7. range.selectNodeContents(referenceNode);
用于设置 Range,使其包含一个 Node 的内容
8. range.setEnd(endNode, endOffset);
设置光标的结束点
9. range.setEndAfter(referenceNode)
设置光标的结束点在node节点之后
10. range.setEndBefore(referenceNode)
设置光标的结束点在node节点之前
11. range.setStart(startNode, startOffset);
设置 Range的开始位置
12. range.setStartAfter(referenceNode)
设置range的开始位置在node节点之后
13. range.setStartBefore(referenceNode)
设置range的开始位置在node节点之前
14. range.surroundContents(newParent);
将 Range 对象的内容移动到一个新的节点,并将新节点放到这个范围的起始处。
实现编辑区插入表情图片
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
padding: 0;
margin: 0;
}
.wrap {
position: relative;
width: 500px;
height: 200px;
overflow: hidden;
overflow-y: auto;
margin-left: 100px;
background-color: #f2a5a5;
margin-top: 20px;
box-sizing: border-box;
padding: 10px;
}
.wrap .box {
width: 100%;
border: none;
line-height: 28px;
}
.show {
width: 500px;
height: 200px;
margin-left: 100px;
border: none;
border: 1px solid #000;
line-height: 28px;
margin-top: 20px;
}
.box[contenteditable] {
outline: none;
}
.box img, .show img {
width: 28px;
vertical-align: middle;
}
.btn, .show_btn {
width: 60px;
height: 30px;
text-align: center;
line-height: 30px;
margin-left: 300px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="box" contenteditable="true">
</div>
</div>
<button class="btn">插入表情</button>
<div class="show"></div>
<button class="show_btn">显示内容</button>
<script src="./index.js"></script>
</body>
</html>
index.js
var wrap = document.querySelector('.wrap'); // 编辑框的外层
var box = document.querySelector('.box'); // 编辑框
var sendEmoji = document.querySelector('.btn'); // 发送按钮
var img = document.createElement('img'); // 要插入的表情img
img.src = ' https://www.kktv5.com/images/facepic/good.gif';
var currentRange; // 记录光标位置
// 点击编辑框时,记录下光标位置
box.onclick = function () {
var selection = window.getSelection();
currentRange = selection.getRangeAt(0);
}
// 上下左右键 enter 输入的内容等键也会改变光标位置,也要记录下来
box.onkeyup = function () {
var selection = window.getSelection();
currentRange = selection.getRangeAt(0);
}
// 插入表情
sendEmoji.onclick = function () {
box.focus();
// 创建一个新的选区
var selection = window.getSelection();
// 如果光标位置原来就存在,就用原来的。
// 原来不存在,就重新创建一个
var range = currentRange ?? selection.getRangeAt(0);
// 插入表情图标
range.insertNode(img.cloneNode());
// 插入后,光标显示在表情图片后面
range.collapse();
// 移除其他的区域
selection.removeAllRanges();
// 把带有表情图片的区域插入到选区内
selection.addRange(range);
}
var show = document.querySelector('.show');
var show_btn = document.querySelector('.show_btn');
show_btn.onclick = function () {
// 把编辑区的内容展示到另一个地方
// 这里是用来模拟的接收人看到的结果
show.innerHTML = box.innerHTML;
}
// 用一层wrap包裹住编辑区,目的是解决光标无法自动捕捉表情图片的问题
// 这里点击wrap,让光标也能识别表情图片
wrap.onclick = function () {
var selection = window.getSelection();
if (currentRange) {
selection.removeAllRanges();
selection.addRange(currentRange);
} else {
selection.selectAllChildren(box);
selection.collapseToEnd();
}
}
参考资料
section
https://developer.mozilla.org/zh-CN/docs/Web/API/Selection
range
https://developer.mozilla.org/zh-CN/docs/Web/API/Range