手机端聊天框实现图片表情连续插入
效果
直接显示表情,且可以在文字中间插入表情,包括换行显示。
难点
要想实现文字图片混排,需要用到富文本编辑技术(即添加contenteditable="true"
属性),但是使用document.execCommand
命令会让文本框会获得焦点,使表情选择框隐藏,如果在插入之后使文本框失去焦点,在苹果手机会因为软键盘弹出收起而导致页面跳动,为了解决这个问题,只能抛弃execCommand
命令,把图片直接插入到指定位置。 ps:富文本编辑器会单独写一篇记录,聊天框使用的只是一个简易版富文本编辑器
需要用到的API
contenteditable 设计模式,设置元素是否是可编辑
document.execCommand 当一个HTML文档切换到设计模式时,允许运行命令来操纵可编辑内容区域的元素
Window.getSelection() 获取用户选择的文本范围或光标的当前位置
Range 表示一个包含节点与文本节点的一部分的文档片段
Node.insertBefore() 在一个节点之前插入一个拥有指定父节点的子节点
Node.appendChild() 将一个节点附加到指定父节点的子节点列表的末尾处
Text.splitText() 根据指定的偏移量将一个
Text
节点分割成前后两个独立的兄弟节点Node.nextSibling 返回其父节点的
childNodes
列表中紧跟在其后面的节点element.onpaste 粘贴事件(需要格式化粘贴的内容,不然会有html)
实现
本示例使用的vue
,如是其他请自行转换,关键代码都是原生js实现。
html
<div>
<div class="editorBox">
<!-- 输入框 -->
<div
class="editor"
ref="chatTextarea"
id="chatTextarea"
contenteditable="true"
@paste="editorPaste"
@blur="editorBlur"
@focus="editorFocus"
@input="editorInput">
</div>
<!-- 表情开关 -->
<div class="icon" @click="openEmoticon">表情</div>
<!-- 发送按钮 -->
<div class="send" @click="submitSend">发送</div>
</div>
<!-- 表情区域 -->
<div class="emoticonBox" v-show="emoticonShow">
<img
class="emoji"
v-for="(v,k) in emojiData"
:src="'static/emoticon/emoji/'+v+'.png'"
@click="chooseEmoji(k)"
:key="k" />
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
js
// ...
export default {
data() {
return {
// ...
emoticonShow: false,
emojiData: {}, // 自己的表情数据
currentSelection: {
startContainer: null,
startOffset: 0,
endContainer: null,
endOffset: 0
},
}
},
mounted() {
// ...
this.init();
},
methods: {
// 初始化编辑器
init(){
const editor = this.$refs.chatTextarea;
// 光标位置为开头
this.currentSelection = {
startContainer: editor,
startOffset: 0,
endContainer: editor,
endOffset: 0
}
// 设置光标位置
this.restorerange();
},
// 备份当前光标位置
backuprange() {
let selection = window.getSelection();
if (selection.rangeCount > 0) {
let range = selection.getRangeAt(0);
this.currentSelection = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
}
},
// 设置光标位置
restorerange() {
if(this.currentSelection){
let selection = window.getSelection();
selection.removeAllRanges();
let range = document.createRange();
range.setStart(this.currentSelection.startContainer, this.currentSelection.startOffset);
range.setEnd(this.currentSelection.endContainer, this.currentSelection.endOffset);
// 向选区中添加一个区域
selection.addRange(range);
}
},
// 插入text文本
inserText(text) {
// 插入前先恢复上次的光标位置
this.restorerange();
document.execCommand('insertText', false, text);
},
// 插入html片段
insertHTML(html) {
// 插入前先恢复上次的光标位置
this.restorerange();
document.execCommand('insertHTML', false, html);
},
// 粘贴事件
editorPaste(e){
// 阻止默认事件
e.preventDefault();
const clp = e.clipboardData || e.originalEvent && e.originalEvent.clipboardData;
// 获取纯文本与富文本
let plainText,htmlText;
if (clp == null) {
plainText = window.clipboardData && window.clipboardData.getData('text');
} else {
plainText = clp.getData('text/plain');
htmlText = clp.getData('text/html');
}
// 某些ios手机获取不到富文本,需要处理一下
if (!htmlText && plainText) {
htmlText = '<div>' + plainText.replace(/[\n\r]|[\r\n]|[\r]|[\n]/g,'</div><div>') + '</div>';
}
if (!htmlText) {
return;
}
// 聊天框只使用纯文本,富文本编辑器会用到html
this.inserText(plainText);
// 备份当前光标
this.backuprange();
},
// 失去焦点
editorBlur(){
// 备份当前光标位置
this.backuprange();
},
// 获取焦点
editorFocus(){
// 处理获取到焦点的逻辑
// ...
// 隐藏表情区域
this.emoticonShow = false;
},
// 输入中
editorInput(){
// 备份当前光标位置
this.backuprange();
},
// 打开表情
openEmoticon(){
this.emoticonShow = true;
},
// 选择插入表情/图片
chooseEmoji(key){
const node = new Image();
node.src = '你需要插入的图片地址';
if(this.currentSelection.startContainer.nodeType==3){
// 如果是文本节点,拆分文字
const newNode = this.currentSelection.startContainer.splitText(this.currentSelection.startOffset);
// 设置光标开始节点为拆分之后节点的父级节点
this.currentSelection.startContainer = newNode.parentNode;
// 在拆分后得到的节点之前插入图片
this.currentSelection.startContainer.insertBefore(node, newNode);
}else{
// 非文本节点
if(this.currentSelection.startContainer.childNodes.length){
// 如果光标开始节点下有子级,获取到光标位置的节点
const beforeNode = this.currentSelection.startContainer.childNodes[this.currentSelection.startOffset];
// 插入
this.currentSelection.startContainer.insertBefore(node, beforeNode);
}else{
// 如果光标开始节点下没有子级,直接插入
this.currentSelection.startContainer.appendChild(node);
}
}
// 获取插入的节点所在父级下的位置
const index = Array.from(this.currentSelection.startContainer.childNodes).indexOf(node);
this.currentSelection.startOffset = index+1;
this.currentSelection.endOffset = index+1;
// 视图滚动带完全显示出来
node.scrollIntoView(false)
},
// 发送
submitSend(){
const html = this.$refs.chatTextarea.innerHTML;
// 发送逻辑
},
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
css
按照设计稿自行添加
最后
主要逻辑都有注释,有更好的实现方式或有问题欢迎留言,我会持续更新~