以上两种状态虽然表现形式不同,但它们在Web领域都叫做Selection。Selection描述的正是网页中的“当前选择”。闪烁的光标也算作一种特殊的选择,称为已折叠(Collapsed)的选择。
Selection在JavaScript中对应的是Selection对象,它可以通过window.getSelection()或document.getSelection()获取到。
任何时候,当网页中“当前选择”发生改变时,都会触发document.onselectionchange事件。这个事件处理函数仅存在于document。
Range的设计意义
Selection已经表示了“当前选择”,那Range是做什么的呢?简单来说,Range是Selection的“预备军”,它和Selection类似,都可以具体描述网页中的“选择”状态,只是Selection是可见的,Range是不可见的。
通过Selection的和Range有关的方法,可以把Range应用到Selection,这时候就可以看到Range的选择效果了。这就好像Selection代表了舞台,Range则是一个又一个幕后的演员,它们可以轮换上场和退场。
除Firefox外,其他浏览器的Selection都只支持单个Range,因此,我们一般在同一时间只能应用一个Range到Selection。
一个Range由两个边界点组成,分别是起始边界点和结尾边界点。这两个边界点在一起,就可以描述任意的“选择”状态。当两个边界点完全相同时,这个“选择”状态就称为折叠的(Collapsed),也就是闪烁光标的状态。
Selection的和Range有关的方法很重要,具体如下:
-
getRangeAt(i) - 按索引获取Selection的当前Range。除Firefox外,其他浏览器只固定使用索引0。
-
addRange(range) - 将range应用到Selection。除Firefox外,如果Selection当前已经有其他Range,将忽略此方法调用。
-
removeRange(range) - 从Selection中取消应用range。
-
removeAllRanges() - 取消应用所有Range。
-
empty() - 等同于removeAllRanges()。
关于Selection和Range的更详细的介绍和说明,推荐阅读这篇Selection And Range。
符合光标位置的表情插入
了解了Selection和Range的基础知识后,我们继续来完成微博风格的表情输入。前面说过,要点是表情HTML代码的插入位置要符合输入框的光标位置,所以我们首先要做的就是记录这个光标位置。
先标记输入框为inputBox(本示例使用Vue):
ref=“inputBox”
class=“input-box”
contenteditable=“true”>
然后使用前面提到的document.onselectionchange监听选择变化事件:
document.onselectionchange = () => {
let selection = document.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
if (vmEmoji.$refs.inputBox.contains(range.commonAncestorContainer)) {
rangeOfInputBox = range;
}
}
};
这段代码的作用是,在“当前选择”发生变化(鼠标点击或触摸动作等)后,如果变化后的Selection位于输入框inputBox内部,就用变量rangeOfInputBox保存它。这里也可以看到,Selection是用Range来保存的。
selection.rangeCount是Selection的属性,它表示Selection正在应用的Range数目。当它大于0时,说明当前是“有选择”的状态。
range.commonAncestorContainer是Range的属性,它表示Range的两个边界点的距离最近的共同父元素。这里用于判断Range发生在inputBox内。
最后,当点击表情时,执行插入表情的方法insertEmoji:
insertEmoji (name) {
let emojiEl = document.createElement(“img”);
emojiEl.src = ${this.emoji.path}${name}${this.emoji.suffix}
;
if (!rangeOfInputBox) {
rangeOfInputBox = new Range();
rangeOfInputBox.selectNodeContents(this.$refs.inputBox);
}
if (rangeOfInputBox.collapsed) {
rangeOfInputBox.insertNode(emojiEl);
} else {
rangeOfInputBox.deleteContents();
rangeOfInputBox.insertNode(emojiEl);
}
rangeOfInputBox.collapse(false);
}
这段代码中,参数name代表了不同表情,从而生成不同表情对应的不同HTML元素(都是)。
如果rangeOfInputBox不存在,说明还没有过任何发生在输入框内的选择事件,此时就指定一个默认的Range。selectNodeContents(node)是Range的方法,将一个Range设定为选中整个node元素内容。
insertNode(node)是Range的方法,可以将node元素插入到Range的起始边界点。它是本示例的关键方法,用于完成表情HTML元素插入。这里需要对Range的状态做判断,如果Range是折叠的(闪烁光标),直接插入表情元素,如果Range不是折叠的(选中了一部分输入框内容),就先删除选中的内容,再插入表情元素(相当于替换内容的效果)。deleteContent()也是Range的方法,可以将Range包含的内容从网页文档中删除。
结尾调用的collapse(toStart)仍然是Range的方法,它可以将Range的两个边界点变成相同的,也就是折叠的状态。如果参数toStart为true则取起始边界点的位置,如果为false则是取结尾边界点。这里取的是结尾边界点,这样就好像是在插入一个表情后,自动将光标移动到刚插入的表情元素后方,从而支持表情的连续输入。
到此,微博风格的表情输入就已经实现了:
把输入框内的内容作为HTML代码(富文本),就可以提交给后台,或者像图里这样简单展示在上方的聊天窗口内。
完善点击表情时的光标置位
这种文字和表情图混合在一起的风格还存在一个待完善的地方:如果点击文字,光标会正确定位到选中的文字前方,而点击表情图,就没有任何动作。这个光标置位的功能我们可以手动补全。
为输入框增加click事件处理:
ref=“inputBox”
@click=“handleBoxClick”
class=“input-box”
contenteditable=“true”>
对应的handleBoxClick()事件处理方法如下:
handleBoxClick (event) {
let target = event.target;
this.setCaretForEmoji(target);
},
setCaretForEmoji (target) {
if (target.tagName.toLowerCase() === “img”) {
let range = new Range();
range.setStartBefore(target);
range.collapse(true);
document.getSelection().removeAllRanges();
document.getSelection().addRange(range);
}
},
setStartBefore(node)是Range的方法,可以设定边界起始点的位置到一个元素之前。这段代码整体来说就是,如果当前click的是元素,就创建一个Range,设定它为折叠状态,位置在刚才点击的表情图之前,然后应用这个Range到Selection,变成真实可见的选择效果。
用纯文本符号来替代表情的场景
现在,我们重新开始,来实现微信风格的表情输入。
前面说过,微信是使用类似[旺柴]这样的符号标识来替代表情的风格。这种风格全部使用纯文本,因此,输入框会很容易实现,可以直接使用表单元素的文本输入框:
<input
ref=“formInput”
@keydown=“handleFormInputKeydown”
class=“form-input”
type=“text”>
这里预留的handleFormInputKeydown()输入事件处理方法,将在后文中使用。
和微博风格类似,接下来也是可以分成两个实现要点:
-
点击下方的表情,就将该表情对应的纯文本符号插入到输入框。
-
纯文本符号的插入位置要符合输入框的当前光标位置。
虽然同样是结合Selection和Range的概念,按光标位置来插入纯文本符号,但会更加简单。
按光标位置来插入纯文本
表单元素自身有以下3个属性是关于“选择”的:
-
input.selectionStart - 选择的起始位置。它的值是一个索引数字,比如6。
-
input.selectionEnd - 选择的结尾位置。值的格式同上。
-
input.selectionDirection - 选择的方向。可选值"forward",“backward"和"none”。一般对应的情况是指鼠标拖拽选择时是从前向后,还是从后向前,又或者是双击选中。
通过这些属性,就可以实现对“选择”状态的读取和写入,而无需使用Selection和Range。
现在,点击表情时,执行插入表情的方法insertEmojiText:
insertEmojiText (name) {
let input = this.$refs.formInput;
let emojiText = [${name}]
;
input.focus();
input.setRangeText(emojiText, input.selectionStart, input.selectionEnd, “end”);
input.blur();
}
可以看到纯文本的表情插入非常简单。这里也是用[name]的符号来表示表情。
input.setRangeText(replacement, [start], [end], [selectionMode])是input的方法,可以将索引位置从start到end的文本,替换成replacement的文本。而如果start等于end,就相当于闪烁光标的状态,没有文本会被替换,变成了插入文本的效果。末尾参数selectionMode决定了在文本替换(或插入)操作完毕后,input如何更新选择状态。这里取"end"表示将选择状态设定为“闪烁光标,位置在新插入文本的后方”,从而支持表情连续输入。
使用input.setRangeText(),无论当前状态是闪烁光标,还是已经选择了一些文本,都会以符合我们输入习惯的方式插入表情文本。
关于input.setRangeText()的更详细的说明,同样推荐阅读这篇Selection And Range。
这段代码中的input.focus()和input.blur(),是因为仅在元素被focus的情况下进行文本编辑操作,才能确保input.selectionStart和input.selectionEnd两个值正确更新。同时,这里又并不希望元素被真地focus,所以又用了input.blur()来取消。
到这里,微信风格的表情输入就基本可用了。但是,这种纯文本符号的风格也有一个应完善的地方:用退格键(Backspace)来删除文本时,代表一个表情的纯文本符号应该以作为一个整体被删除。比如[旺柴]这样的表情符号,在光标位于]的后方时,一个退格键就应该删除这一整段文本。这也是微信里存在的功能。
退格键支持 - 以表情符号为整体删除文本
前文示例中为元素预留的handleFormInputKeydown()方法,就是用于实现这一功能:
handleFormInputKeydown (event) {
let input = this.$refs.formInput;
let chatString = input.value;
// “Backspace” and selection type “Caret”
if (event.keyCode === 8 && input.selectionStart === input.selectionEnd) {
let indexEnd = input.selectionStart - 1;
let charToDelete = chatString.charAt(indexEnd);
// delete the whole [***]
if (charToDelete === “]”) {
event.preventDefault();
let indexStart = chatString.lastIndexOf(“[”, indexEnd);
input.setRangeText(“”, indexStart, indexEnd + 1, “end”);
}
}
}
这段代码是判断当选择状态为闪烁光标,且刚好位于字符]后按下了退格键的时候,就找出整个[name]表情文本,使用input.setRangeText()实现整段删除。
到此,微信风格的表情输入也就完成了:
在提交给后台或者图中这样展示在上方聊天窗口内的时候,取输入框内的纯文本,然后将所有[name]格式的文本符号,替换成对应表情的HTML(比如[1]变成)即可。
完整代码示例
两种风格的完整代码示例:
-
微博风格(表情图和文字一起)
-
https://codesandbox.io/s/emoji-input-contenteditable-75qe8
-
微信风格(表情用纯文本符号替代)
最后
推荐一些系统学习的途径和方法。
每个Web开发人员必备,很权威很齐全的Web开发文档。作为学习辞典使用,可以查询到每个概念、方法、属性的详细解释,注意使用英文关键字搜索。里面的一些 HTML,CSS,HTTP 技术教程也相当不错。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
HTML 和 CSS: