前端实现常见的输入框@功能,或是插入话题功能,我们先只考虑textarea控件中的实现思路,难点是文本框的光标位置,在整个页面中的坐标获取。
网上搜了一圈,整个位置的获取是通过克隆一个div,把文本框的内容实时同步更新到div中,光标的位置用一个span标签表示,然后获取span标签的offset位置,从而计算出文本框中真实光标的位置。
代码如下:自行运行,不是很完善,感兴趣的可二次加工,因为有更好的方案插件,这个只作为技术调研。
css
` * { margin: 0; padding: 0; } body { font-size: 12px; } ul, li { list-style: none; }
#txtDom,
#clonetxt {
width: 200px;
height: 60px;
/* font-size: 14px; */
line-height: 140%;
padding: 5px;
/* 字体保持一致 否则 */
/* font-family: auto; */
word-break: break-all;
resize: none;
overflow-y: auto;
}
::-webkit-scrollbar {
-webkit-appearance: none;
width: 7px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
}
`
html
`
<div style="width: 300px; margin: 0 auto; padding: 100px 0 0">
<textarea id="txtDom"></textarea>
<div id="clonetxt" style="background: #ccc; position: absolute"></div>
<div
id="tipsAtDom"
style="
border: 1px solid #f60;
color: #999;
background: #fff;
position: absolute;
display: none;
"
>
<input type="text" placeholder="可搜索" />
<ul>
<li>小明</li>
<li>李磊</li>
<li>韩梅梅</li>
</ul>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
js
` var txtDom = $("#txtDom");
var tipsAtDom = $("#tipsAtDom");
var cloneDiv = $("#clonetxt");
cloneDiv.css({
width: txtDom.width(),
height: "auto",
"font-size": txtDom.css("font-size"),
"font-family": txtDom.css("font-family"),
});
var gbspan = '<span class="gbspan" style="color:red;">|</span>';
// 浮层备选点击事件
tipsAtDom.on("click", "li", function () {
var domVal = txtDom.val();
var txt = $(this).text();
const { selectionStart, selectionEnd } = txtDom[0];
const newStr = `${domVal.substring(
0,
selectionStart
)}${txt} ${domVal.substring(selectionEnd, domVal.length)}`;
txtDom.val(newStr);
tipsAtDom.hide();
setCursorPosition(txtDom[0], selectionStart + txt.length + 1);
// txtDom.focus() //上面方法已经执行了focus
// 定位滚动条到之前 因为focus会让滚动条触底 keyup是为了让clone div光标位置更新
txtDom.scrollTop(txtDom.data("scoltop")).trigger("keyup");
});
// 文本框事件绑定
txtDom
// 键盘抬起
.on("keyup click", function (e) {
$(this).data("scoltop", $(this).scrollTop());
this.startPos = e.target.selectionStart;
this.endPos = e.target.selectionEnd;
const originalStr = e.target.value;
const htmlStr =
this.selectionStart === e.target.textLength
? `${originalStr}${gbspan}`
: `${originalStr.substring(
0,
this.startPos
)}${gbspan}${originalStr.substring(
this.endPos,
originalStr.length
)}`;
cloneDiv.html(htmlStr.replace(/\n/g, "<br/>"));
// console.log(this.value);
})
// 键盘按下
.on("keydown", function (e) {
var { key, keyCode } = e;
console.log("key:", key, "keyCode:", keyCode);
// console.log(e);
// console.log(this.value)
if (["@", "#"].includes(key)) {
console.log("ok @");
tipsAtDomShow();
} else {
tipsAtDom.hide();
}
});
// 显示候选框 设置pos位置
function tipsAtDomShow() {
const $span = cloneDiv.find(".gbspan:first");
var gbPos = $span.length ? $span.position() : { left: 0, top: 0 };
const txtDomPos = txtDom.offset();
console.log("gbPos:", gbPos, txtDomPos);
tipsAtDom
.css({
left: txtDomPos.left + gbPos.left + 12 + "px",
top: txtDomPos.top + gbPos.top + 25 - txtDom.scrollTop() + "px",
})
.show();
}
/**
* 文本框根据输入内容自适应高度
* @param {HTMLElement} 输入框元素
* @param {Number} 设置光标与输入框保持的距离(默认0)
* @param {Number} 设置最大高度(可选)
*/
var autoTextarea = function (elem, extra, maxHeight) {
extra = extra || 0;
var isFirefox =
!!document.getBoxObjectFor || "mozInnerScreenX" in window,
isOpera =
!!window.opera && !!window.opera.toString().indexOf("Opera"),
addEvent = function (type, callback) {
elem.addEventListener
? elem.addEventListener(type, callback, false)
: elem.attachEvent("on" + type, callback);
},
getStyle = elem.currentStyle
? function (name) {
var val = elem.currentStyle[name];
if (name === "height" && val.search(/px/i) !== 1) {
var rect = elem.getBoundingClientRect();
return (
rect.bottom -
rect.top -
parseFloat(getStyle("paddingTop")) -
parseFloat(getStyle("paddingBottom")) +
"px"
);
}
return val;
}
: function (name) {
return getComputedStyle(elem, null)[name];
},
minHeight = parseFloat(getStyle("height"));
elem.style.resize = "none";
var change = function () {
var scrollTop,
height,
padding = 0,
style = elem.style;
if (elem._length === elem.value.length) return;
elem._length = elem.value.length;
if (!isFirefox && !isOpera) {
padding =
parseInt(getStyle("paddingTop")) +
parseInt(getStyle("paddingBottom"));
}
scrollTop =
document.body.scrollTop || document.documentElement.scrollTop;
elem.style.height = minHeight + "px";
if (elem.scrollHeight > minHeight) {
if (maxHeight && elem.scrollHeight > maxHeight) {
height = maxHeight - padding;
style.overflowY = "auto";
} else {
height = elem.scrollHeight - padding;
style.overflowY = "hidden";
}
style.height = height + extra + "px";
scrollTop += parseInt(style.height) - elem.currHeight;
document.body.scrollTop = scrollTop;
document.documentElement.scrollTop = scrollTop;
elem.currHeight = parseInt(style.height);
}
};
addEvent("propertychange", change);
addEvent("input", change);
addEvent("focus", change);
change();
};
/*
* 设置输入域(input/textarea)光标的位置
* @param {HTMLInputElement/HTMLTextAreaElement} elem
* @param {Number} index
*/
function setCursorPosition(elem, index) {
var val = elem.value;
var len = val.length;
// 超过文本长度直接返回
if (len < index) return;
// setTimeout(function() {
elem.focus();
if (elem.setSelectionRange) {
// 标准浏览器
elem.setSelectionRange(index, index);
} else {
// IE9-
var range = elem.createTextRange();
range.moveStart("character", -len);
range.moveEnd("character", -len);
range.moveStart("character", index);
range.moveEnd("character", 0);
range.select();
}
// }, 10)
}
autoTextarea(txtDom[0], 0, 200);
`