没有考虑浏览器兼容性,暂时只考虑了Chrome。
效果
在div中输入的时候,底下的蓝色部分(span)跟随光标变化而变化,而且内容为光标前的所有文本。
红色的部分是一个|,用来模拟光标。所以只要获得|的相对像素位置,以及div的位置,就可以计算出光标的绝对像素位置。还有个前提是蓝色部分的样式要和div文字的样式相同。
div获取光标位置比较复杂,如果是textarea和input框,内部是纯文本,比较好处理。
div中输入的时候,第一行输入的内容是div的第一个文本元素。换行之后,每一行都会自动被一个div包裹起来。如果该行为空,默认里面会有一个br标签,如果有内容,则br标签会消失。
select对象的anchorNode,是一个#text,通过它可以找到光标作用在div内部某一个div,也就是某一行,记作y坐标。而anchorOffset,记录了光标在这个#text的偏移量,记作x坐标。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="inputBox" contenteditable="true" oninput="handleInput(this)"></div>
<span style="background: #409EFF;" id="fakeContent"></span><!--
两个span之间用注释隔开,消除空隙(不能用浮动)
--><span style="background: crimson;" id="fakeCursor">|</span>
<br/>
</body>
</html>
<script>
let fakeContent = document.querySelector('#fakeContent');
/**
* 输入事件
* */
function handleInput(that) {
let cursorCoordinate = getCursorCoordinate(that);
//文本数组,div每一行都是一个string
let contentStrList = getNodeList(that).map(node => node.textContent);
let spanContent = '';
//光标y坐标也就是contentStrList的索引
let y = cursorCoordinate.y;
for (let i = 0; i < y; i++) {
//添加内容和换行
spanContent += contentStrList[i]+'<br/>';
}
if(contentStrList.length > 0) {
//如果长度为0,表示编辑框没有内容,y会是-1,contentStrList[y]会是undefined
//按照光标偏移量截取字符串
spanContent += contentStrList[y].substr(0,cursorCoordinate.x);
}
fakeContent.innerHTML = spanContent;
}
/**
* 获取div内容并转为Node数组
* @param elem
* @returns {ChildNode[]}
*/
function getNodeList(elem) {
return Array.from(elem.childNodes);
}
/**
* 获取光标的坐标
* @param elem
* @returns {{x: number 光标偏移量, y: number 光标在div子元素数组中的索引}}
*/
function getCursorCoordinate(elem) {
const selection = window.getSelection();
const cursorIndex = getNodeList(elem).findIndex(node =>
node.firstChild === selection.anchorNode|| node === selection.anchorNode);
const cursorOffset = selection.anchorOffset;
return {
x: selection.anchorOffset,
y: cursorIndex
}
}
</script>