一个@user的Vue组件,仿微博聊天@user
Web聊天工具的富文本输入框
Github地址
难点:
- 获取位置,在合适的地方插入@列表
- 文本插入,找到对应的位置插入选中的文本
Github
获取键盘码【键盘ASCII码】
1 |
|
获取textarea输入的坐标位置,距离textarea左上角
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<style type="text/css">
* {
padding: 0px;
margin: 0px;
}
#main{
width: 200px;
height: 150px;
position: relative;
}
textarea {
width: 200px;
height: 150px;
resize: none;
position: absolute;
left: 0px;
top: 0px;
}
.test{
width: 5px;
height: 5px;
position: absolute;
left: 0px;
top: 0px;
background: red;
}
</style>
<script type="text/javascript">
(function() {
var properties = [
'direction', // RTL support
'boxSizing',
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
'height',
'overflowX',
'overflowY', // copy the scrollbar for IE
'borderTopWidth',
'borderRightWidth',
'borderBottomWidth',
'borderLeftWidth',
'borderStyle',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
'fontStyle',
'fontVariant',
'fontWeight',
'fontStretch',
'fontSize',
'fontSizeAdjust',
'lineHeight',
'fontFamily',
'textAlign',
'textTransform',
'textIndent',
'textDecoration', // might not make a difference, but better be safe
'letterSpacing',
'wordSpacing',
'tabSize',
'MozTabSize'
];
var isBrowser = (typeof window !== 'undefined');
var isFirefox = (isBrowser && window.mozInnerScreenX != null);
function getCaretCoordinates(element, position, options) {
if(!isBrowser) {
throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
}
var debug = options && options.debug || false;
if(debug) {
var el = document.querySelector('#input-textarea-caret-position-mirror-div');
if(el) el.parentNode.removeChild(el);
}
var div = document.createElement('div');
div.id = 'input-textarea-caret-position-mirror-div';
document.body.appendChild(div);
var style = div.style;
var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
var isInput = element.nodeName === 'INPUT';
style.whiteSpace = 'pre-wrap';
if(!isInput)
style.wordWrap = 'break-word';
style.position = 'absolute';
if(!debug)
style.visibility = 'hidden';
properties.forEach(function(prop) {
if(isInput && prop === 'lineHeight') {
if(computed.boxSizing === "border-box") {
var height = parseInt(computed.height);
var outerHeight =
parseInt(computed.paddingTop) +
parseInt(computed.paddingBottom) +
parseInt(computed.borderTopWidth) +
parseInt(computed.borderBottomWidth);
var targetHeight = outerHeight + parseInt(computed.lineHeight);
if(height > targetHeight) {
style.lineHeight = height - outerHeight + "px";
} else if(height === targetHeight) {
style.lineHeight = computed.lineHeight;
} else {
style.lineHeight = 0;
}
} else {
style.lineHeight = computed.height;
}
} else {
style[prop] = computed[prop];
}
});
if(isFirefox) {
if(element.scrollHeight > parseInt(computed.height))
style.overflowY = 'scroll';
} else {
style.overflow = 'hidden';
}
div.textContent = element.value.substring(0, position);
if(isInput)
div.textContent = div.textContent.replace(/\s/g, '\u00a0');
var span = document.createElement('span');
span.textContent = element.value.substring(position) || '.';
div.appendChild(span);
var coordinates = {
top: span.offsetTop + parseInt(computed['borderTopWidth']),
left: span.offsetLeft + parseInt(computed['borderLeftWidth']),
height: parseInt(computed['lineHeight'])
};
if(debug) {
span.style.backgroundColor = '#aaa';
} else {
document.body.removeChild(div);
}
return coordinates;
}
if(typeof module != 'undefined' && typeof module.exports != 'undefined') {
module.exports = getCaretCoordinates;
} else if(isBrowser) {
window.getCaretCoordinates = getCaretCoordinates;
}
}());
</script>
</head>
<body>
<div id="main">
<textarea cols="30" rows="10" wrap="hard"></textarea>
<div class="test"></div>
</div>
</body>
<script type="text/javascript">
document.querySelector('textarea').addEventListener('input', function(e) {
console.log( e.keyCode)
var caret = getCaretCoordinates(this, this.selectionEnd);
$(".test").css({
left: caret.left+'px',
top: caret.top+'px'
})
console.log('(top, left, height) = (%s, %s, %s)', caret.top, caret.left, caret.height);
})
</script>
</html>
获取textarea鼠标在字符串的位置
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<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title>JS设置及获取Textarea的光标位置</title>
<script>
var isIE = !(!document.all); //是不是IE
function posCursor() {
var start = 0,
end = 0;
var oTextarea = document.getElementById("textarea");
if(isIE) {
//selection 当前激活选中区,即高亮文本块,和/或文当中用户可执行某些操作的其它元素。
//createRange 从当前文本选中区中创建 TextRange 对象,
//或从控件选中区中创建 controlRange 集合。
var sTextRange = document.selection.createRange();
//判断选中的是不是textarea对象
if(sTextRange.parentElement() == oTextarea) {
//创建一个TextRange对象
var oTextRange = document.body.createTextRange();
//移动文本范围以便范围的开始和结束位置能够完全包含给定元素的文本。
oTextRange.moveToElementText(oTextarea);
//此时得到两个 TextRange
//oTextRange文本域(textarea)中文本的TextRange对象
//sTextRange是选中区域文本的TextRange对象
//compareEndPoints方法介绍,compareEndPoints方法用于比较两个TextRange对象的位置
//StartToEnd 比较TextRange开头与参数TextRange的末尾。
//StartToStart比较TextRange开头与参数TextRange的开头。
//EndToStart 比较TextRange末尾与参数TextRange的开头。
//EndToEnd 比较TextRange末尾与参数TextRange的末尾。
//moveStart方法介绍,更改范围的开始位置
//character 按字符移动
//word 按单词移动
//sentence 按句子移动
//textedit 启动编辑动作
//这里我们比较oTextRange和sTextRange的开头,的到选中区域的开头位置
for(start = 0; oTextRange.compareEndPoints("StartToStart", sTextRange) < 0; start++) {
oTextRange.moveStart('character', 1);
}
//需要计算一下\n的数目(按字符移动的方式不计\n,所以这里加上)
for(var i = 0; i <= start; i++) {
if(oTextarea.value.charAt(i) == '\n') {
start++;
}
}
//再计算一次结束的位置
oTextRange.moveToElementText(oTextarea);
for(end = 0; oTextRange.compareEndPoints('StartToEnd', sTextRange) < 0; end++) {
oTextRange.moveStart('character', 1);
}
for(var i = 0; i <= end; i++) {
if(oTextarea.value.charAt(i) == '\n') {
end++;
}
}
}
} else {
start = oTextarea.selectionStart;
end = oTextarea.selectionEnd;
}
document.getElementById("start").value = start;
document.getElementById("end").value = end;
}
function moveCursor() {
var oTextarea = document.getElementById("textarea");
var start = parseInt(document.getElementById("start").value);
var end = parseInt(document.getElementById("end").value);
if(isNaN(start) || isNaN(end)) {
alert("位置输入错误");
}
if(isIE) {
var oTextRange = oTextarea.createTextRange();
var LStart = start;
var LEnd = end;
var start = 0;
var end = 0;
var value = oTextarea.value;
for(var i = 0; i < value.length && i < LStart; i++) {
var c = value.charAt(i);
if(c != '\n') {
start++;
}
}
for(var i = value.length - 1; i >= LEnd && i >= 0; i--) {
var c = value.charAt(i);
if(c != '\n') {
end++;
}
}
oTextRange.moveStart('character', start);
oTextRange.moveEnd('character', -end);
//oTextRange.collapse(true);
oTextRange.select();
oTextarea.focus();
} else {
oTextarea.select();
oTextarea.selectionStart = start;
oTextarea.selectionEnd = end;
}
}
</script>
<body>
<table border="1" cellspacing="0" cellpadding="0">
<tr>
<td>start: <input type="text" id="start" size="3" value="0" /></td>
<td>end: <input type="text" id="end" size="3" value="0" /></td>
</tr>
<tr>
<td colspan="2">
<textarea id="textarea"
onKeydown="posCursor()"
onKeyup="posCursor()"
onmousedown="posCursor()"
onmouseup="posCursor()"
onfocus="posCursor()"
rows="14"
cols="50">虞美人
春花秋月何时了,往事知多少。
小楼昨夜又东风,故国不堪回首月明中!
雕l栏玉砌应犹在,只是朱颜改。
问君能有几多愁?恰似一江春水向东流。
</textarea>
</td>
</tr>
<tr>
<td></td>
<td>
<input type="button" onClick="moveCursor()" value="设置光标位置" />
</td>
</tr>
</table>
</body>
</html>
Code
App.js 入口文件
1 | <template> |
At.vue
1 | <template> |
util.js
1 |
|
js获取光标位置
获取光标位置,设置光标位置
1 | /** |
JS获取文本框焦点光标位置、选中起始位置、终止位置、选择内容
扩展知识
js获取光标位置
1.概念和原理
DOM中并没有直接获取光标位置的方法,那么我们只能间接来获取光标位置。DOM支持获取光标选中的范围,我们可以以此为切入点,来获取或定位光标的位置。当选取范围起始点和结束点一样时,就是光标插入的位置。
1.1 术语
anchor(瞄点)
:选区起点。
focus(焦点)
:选区终点。
range(范围)
:选区范围,包含整个节点或节点的一部分。
1.2 Selection
Selection
:Selection对象
表示用户选择的文本范围或插入符号的位置。
经过实验发现Selection选取的节点范围都是块级节点。input和texteare并不能作为Selection的节点
Selection
对象存在于window
对象上,可以通过window.getSelection()
获取示例。
属性:
anchorNode
:选区起点的节点。
anchorOffset
:选区的起点位置。
focusNode
:选区终点的节点。
focusOffset
:选区的终点位置。
isCollapsed
:起点和终点是否重叠。
rangeCount
:选区包含的range数目。
方法
getRangeAt(index)
:获取指定的选取范围。
addRange(range)
:将一个范围添加到Selection对象中。
removeRange()
:移出指定的范围。
removeAllRanges()
:移出所有range对象。
collapse(parentNode,offset)
:将光标移动到parentNode节点的offset位置。
collapseToStart()
:取消当前选区,并把光标定位在原选区的最开始处,如果此时光标所处的位置是可编辑的,且它获得了焦点,则光标会在原地闪烁。
collapseToEnd()
:取消当前选区,并将光标定位到原选取的最末位。如果此时光标所处的位置是可编辑的,且它获得了焦点,则光标会在原地闪烁。
extend(node,offset)
:将终点位置移动到node节点的offset位置。
modify(alter,direction,granularity)
:通过alter方式(move/extend)
来改变光标位置,移动方向为direction(left/right)
,移动单位为granularity
。
containsNode(aNode,aPartlyContained)
:判断aNode是否包含在Selection中。aPartlyContained为false表示全包含,为true表示只要部分包含即可。
toString()
:放回当前Selection对象的字符串。
1.3 Range
Range
对象表示一个Selection
的选择范围,一个Selection
可以包含多个Range
。
获取对象
document.createRange()
:创建一个Range。
selection.getRangeAt(index)
:获取指定的Range。
属性
collapsed
:判断起始位置是否重合。
endContaniner
:range终点节点。
endOffset
:range的终点位置。
startContaniner
:ranstartge起点节点。
startOffset
:range的起点位置。
commonAncestorContainer
:包含起始点的节点。
方法
setStart(startNode,startOffset)
:设置范围在startNode的起始位置为startOffset。
setEnd(endNode,endOffset)
:设置范围在endNode的起始位置为endOffset。
selectNode(referenceNode)
:设置range的节点为referenceNode。
selectNodeContents(referenceNode)
:设置range的内容为referenceNode。
collapse(toStart)
:向边界点折叠range,即是设置光标位置,toStart默认为false,表示光标定位在节点末尾。true表示光标定位在节点起点。
cloneContents()
:克隆一个range的内容片段。
deleteContents()
:删除range的内容片段。
extractContents()
:将range的内容从文档树移动到文档片段中。
insertNode(newNode)
:在range的其实位置插入新的节点。
surroundContents(newNode)
:将range对象的内容移动到新的节点中。
cloneRange()
:克隆一个range对象。
detach()
:释放当前range。
1.4 input/textarea
在html5中,可输入性表单元素(input/textarea)都存在以下属性。不支持IE6/7。
selectionDirection
:forward | backward | none,选区方向selectionEnd
:选区终点位置selectionStart
:选区起点位置
setSelectionRange(selectionStart, selectionEnd, [selectionDirection])
:设置获取焦点的输入性元素的选区范围。
2.获取光标位置
2.1 可编辑div获取光标位置
1 | //获取当前光标位置 |
获取光标的位置是先通过获取鼠标的选取范围,然后克隆该选取范围,修改克隆范围的结束位置,这样克隆的范围就只剩下起点到结束点的内容,光标之后的内容被截取扔掉了。所以可以通过剩余内容的长度来确定光标位置。之所以要克隆一个选取范围出来,是为了避免修改光标结束位置时影响到原先内容。
2.2 input/textarea获取光标位置
1 | //输入框获取光标 |
3.设置光标位置
3.1 可编辑div设置光标位置
1 | //设置光标位置 |
3.2 input/textarea获取光标位置
1 | // 设置光标位置 |
4.示例
1 | <html> |
效果图:
JS获取文本框焦点光标位置、选中起始位置、终止位置、选择内容
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<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
function getSelectPosition(oTxt) {
var nullvalue = -1;
var selectStart; //选中开始位置
var selectEnd; //选中结束位置
var position; //焦点位置
var selectText; //选中内容
if(oTxt.setSelectionRange) { //非IE浏览器
selectStart = oTxt.selectionStart;
selectEnd = oTxt.selectionEnd;
if(selectStart == selectEnd) {
position = oTxt.selectionStart;
selectStart = nullvalue;
selectEnd = nullvalue;
} else {
position = nullvalue;
}
selectText = oTxt.value.substring(selectStart, selectEnd);
} else { //IE
var range = document.selection.createRange();
selectText = range.text;
range.moveStart("character", -oTxt.value.length);
position = range.text.length;
selectStart = position - (selectText.length);
selectEnd = selectStart + (selectText.length);
if(selectStart != selectEnd) {
position = nullvalue;
} else {
selectStart = nullvalue;
selectEnd = nullvalue;
}
}
document.getElementById("txt1").value = position;
document.getElementById("txt2").value = selectStart;
document.getElementById("txt3").value = selectEnd;
document.getElementById("txt4").value = selectText;
}
</script>
<input type="text" id="txt" value="abcdefghijklmn"
onclick="getSelectPosition(this);">
点击文本框内容触发事件<br/>
焦点位置:<input type="text" id="txt1" value=""><br/>
选中起始位置:<input type="text" id="txt2" value="">
选中结束位置<input type="text" id="txt3" value=""><br/>
选中内容: <input type="text" id="txt4" value="">
</body>
</html>
enter键发送,ctrl+enter换行
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
61var keyEnter = document.querySelector('.rongcloud-text');
// 监听事件 键盘按下触发
keyEnter.addEventListener('keydown', function (e) {
e = e || window.event;
// console.log($(".rongcloud-text").val());
var keyCode = e.keyCode || e.which || e.charCode;
var ctrlKey = e.ctrlKey || e.metaKey;
// 判断 ctrl+enter 换行
if (ctrlKey && keyCode == 13) {
var str = $(".rongcloud-text").val();
$(".rongcloud-text").val(str + "\n");
} else if (keyCode == 13) {
// 阻止提交自动换行
e.preventDefault();
// 获取发送按钮id,调用 发送按钮事件
document.getElementById("rong-sendBtn").click();
}
})
// 获取 文本域 并清空空格
var $textarea = $(".rongcloud-text").val().replace(/[ ]/g, "");
// 获取 发送按钮
var $sendBtn = $("#rong-sendBtn");
// 判断当 文本域length == 0,设置按钮颜色
if ($textarea.length == 0) {
// #0099ff
$sendBtn.css('background', 'red');
}
// 当 文本域输入信息 弹起时 给 发送按钮 设置背景颜色(提醒用户进入编辑状态)
$(".rongcloud-text").keyup(function () {
var currentText = $(this).val().replace(/[ ]/g, "");
//console.log(key);
if (currentText.length >= 1) {
$sendBtn.css('background', 'orange');
} else {
$sendBtn.css('background', 'blue');
}
});
/* 点击 发送按钮 注册单击事件 */
$('body').on('click', "#rong-sendBtn", function () {
// 获取 文本域 输入内容
var contentVal = $('.rongcloud-text').val();
// 当信息为空时,不能发空白信息
if (contentVal == "") {
alert("不能发送空白信息");
return false;
}
// 长度 小于 return
if (contentVal.length < 1) {
return false;
}
// 调用 发送函数
sendMssage(targetId, contentVal, doctorName, doctorId);
// 清空文本域的值
$(".rongcloud-text").val("");
});
1 | <!-- 右边聊天窗口 部分 --> |
增加动态文本插入,enter发送,ctrl+enter换行
1 | <template> |
1 | <template> |